<!DOCTYPE HTML>
<html>
<head>
<title>MicroProfile Capture</title>
<style>
/* about css: http://bit.ly/1eMQ42U */
body {margin: 0px;padding: 0px; font: 12px Courier New;background-color:#343434; color:white;overflow:hidden;}
ul {list-style-type: none;margin: 0;padding: 0;}
li{display: inline; float:left;border:5px; position:relative;text-align:center;}
a {
    float:left;
    text-decoration:none;
    display: inline;
    text-align: center;
	padding:5px;
	padding-bottom:0px;
	padding-top:0px;
    color: #FFFFFF;
    background-color: #343434;
}
a:hover, a:active{
	background-color: #000000;
}

ul ul {
    position:absolute;
    left:0;
    top:100%;
    margin-left:-999em;
}
li:hover ul {
    margin-left:0;
    margin-right:0;
}
ul li ul{ display:block;float:none;}
ul li ul li{ display:block;float:none;}
li li a{ display:block;float:none;text-align:left;word-break: keep-all;white-space: nowrap;}
#nav li:hover div {margin-left:0;}
.help {position:absolute;z-index:5;text-align:left;padding:2px;background-color: #313131;width:300px;}
.helpstart {position:absolute;z-index:5;text-align:left;padding:2px;background-color: #313131;width:300px;display:none}
.root {z-index:1;position:absolute;top:0px;left:0px;}
.filterinput0{position:fixed;bottom:10px;left:25px;background-color: #313131}
.filterinput1{position:fixed;bottom:10px;left:175px;background-color: #313131}
.filterinputsearchdiv{position:fixed; background-color: #313131;display:none;}
.filterinputsearchdivmenu{position:absolute; background-color: #313131;display:none;}
.filterinputsearch{width:100px;}
</style>
</head>
<body style="">
<input type="file" id="file-input" style="display:none"/>
<div id='filterinput'>
<div class="filterinput0">Group<br><input type="text" id="filtergroup"></div>
<div class="filterinput1">Timer/Thread<br><input type="text" id="filtertimer"></div>
</div>
<div class="filterinputsearchdiv" id="FilterInputDiv">Filter<br><input type="text" id="FilterInput" class="filterinputsearch"></div>
<div class="filterinputsearchdivmenu" id="FilterInputMenuDiv">Filter<br><input type="text" id="FilterInputMenu" class="filterinputsearch"></div>
<canvas id="History" height="70" style="background-color:#343434;margin:0px;padding:0px;"></canvas>
<canvas id="DetailedView" height="200" style="background-color:#343434;margin:0px;padding:0px;"></canvas>
<canvas id="CanvasMenu" style="pointer-events:none;position:absolute;top:0;left:0;margin:0px;padding:0px;zIndex:1" height="500" width="500"></canvas>
<div class="help" id="divFrameInfo" style="display:none;left:20px;top:300px;width:auto;"></div>
<div class="helpstart" id="helpwindow" style="left:20px;top:20px">
History View:<br>
Click + Drag: Pan View<br>
Right Click + Drag : Zoom on region<br>
Click Frame : Center on frame<br>
<hr>
Main View:<br>
Ctrl + Mouse up/down: Zoom<br>
Mousewheel : Zoom<br>
Right Click + Drag: Select region<br>
Ctrl + Shift + Drag: Select region<br>
Space: Zoom to Selection<br>
Ctrl + Drag: Pan<br>
Click + Drag: Pan<br>
hold alt: Show tooltip from timer view in detailed, detailed in timer<br>
x : Toggle View<br>
<hr>
Detailed View:<br>
Tab: Go To Worst Instance<br>
Left/Right Arror: Next/Prev Instance<br>
Enter: Search for timer in view<br>
<hr>
Timer Views:<br>
Tab: go to filtering<br>
Esc: Exit &amp; Clear filter
<hr>
<table style="width:100%">
<tr>
<td width="50%" align="left"><a href='javascript:void(0)' onclick="ShowHelp(0, 0);">Close</a></td>
<td width="50%" align="right"><a href='javascript:void(0)' onclick="ShowHelp(0, 1);">Close, Never Show</a></td>
</tr>
</table>
</div>
<script>
function ConvertRgbToHsl(hexTripletColor) //from https://gist.github.com/mjackson/5311256
{
	var color = hexTripletColor;
	color = color.substring(1);
	color = parseInt(color, 16);
	var r = ((color >> 16) % 256)/255.0;
	var g = ((color >> 8) % 256)/255.0;
	var b = ((color >> 0) % 256)/255.0;
	var max = Math.max(r, g, b), min = Math.min(r, g, b);
	var h, s, l = (max + min) / 2;
	if (max == min) {
		h = s = 0; // achromatic
	}
	else 
	{
		var d = max - min;
		s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

		switch (max) 
		{
			case r: h = (g - b) / d + (g < b ? 6 : 0); break;
			case g: h = (b - r) / d + 2; break;
			case b: h = (r - g) / d + 4; break;
		}
		h /= 6;
	}
	return [ h, s, l ];
}

function ConvertHslToColor(h, s, l) //from https://gist.github.com/mjackson/5311256
{
	var r, g, b;
	if (s == 0)
	{
		r = g = b = l; // achromatic
	}
	else
	{
		function hue2rgb(p, q, t)
		{
			if (t < 0) t += 1;
			if (t > 1) t -= 1;
			if (t < 1/6) return p + (q - p) * 6 * t;
			if (t < 1/2) return q;
			if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
			return p;
		}
		var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
		var p = 2 * l - q;
		r = hue2rgb(p, q, h + 1/3);
		g = hue2rgb(p, q, h);
		b = hue2rgb(p, q, h - 1/3);
	}
	var color = ((r*255)<<16) | ((g*255)<<8) | (b*255);
	return '#' + ("000000" + color.toString(16)).slice(-6);
}


function ConvertColorDark(hexTripletColor)
{
	var color = hexTripletColor;
	color = color.substring(1); // remove #
	color = parseInt(color, 16); // convert to integer
	// console.log(color.toString(16));
	color = (color >> 1);
	// console.log(color.toString(16));
	color = color & 0x7f7f7f;
	// console.log(color.toString(16));
	return '#' + ("000000" + color.toString(16)).slice(-6);
}
function InvertColor(hexTripletColor) 
{
	var color = hexTripletColor;
	color = color.substring(1); // remove #
	color = parseInt(color, 16); // convert to integer
	var R = ((color >> 16) % 256)/255.0;
	var G = ((color >> 8) % 256)/255.0;
	var B = ((color >> 0) % 256)/255.0;
	var lum = (0.2126*R + 0.7152*G + 0.0722*B);
	if(lum < 0.7)
	{
		return '#ffffff';
	}
	else
	{
		return '#333333';
	}
}
function InvertColorIndex(hexTripletColor) {
	var color = hexTripletColor;
	color = color.substring(1); // remove #
	color = parseInt(color, 16); // convert to integer
	var R = ((color >> 16) % 256)/255.0;
	var G = ((color >> 8) % 256)/255.0;
	var B = ((color >> 0) % 256)/255.0;
	var lum = (0.2126*R + 0.7152*G + 0.0722*B);
	if(lum < 0.7)
	{
		return 0;
	}
	else
	{
		return 1;
	}
}

function MakeGroup(id, name, category, numtimers, isgpu, total, average, max, color)
{
	if(color == "")
	{
		color = ColorFromString(name, 40, 50);
	}
	var cid = GetColorIndex(color);
	var group = {"id":id, "name":name, "category":category, "numtimers":numtimers, "isgpu":isgpu, "total": total, "average" : average, "max" : max, "cid":cid};
	return group;
}

function MakeGroupCid(id, name, category, numtimers, isgpu, total, average, max, color, cid)
{
	var group = {"id":id, "name":name, "category":category, "numtimers":numtimers, "isgpu":isgpu, "total": total, "average" : average, "max" : max, "cid":cid};
	return group;
}

function MakeTimer(id, name, group, color, colordark, average, max, min, exclaverage, exclmax, callaverage, callcount, total, meta, metaavg, metamax)
{
	if(color == "#000000")
	{
		color = ColorFromString(name + "x" + group, 40, 50);
		colordark = ColorFromString(name + "x" + group, 25, 50);
	}
	var spike = max <= 0 || average <= 0 ? 0 : (100 * max / average);
	var cid = GetColorIndex(color);
	var timer = {"id":id, "name":name, "len":name.length, "cid":cid, "timercid":cid, "group":group, "average":average, "max":max, "min":min, "exclaverage":exclaverage, "exclmax":exclmax, "callaverage":callaverage, "callcount":callcount, "spike":spike,"total":total, "meta":meta, "metaavg":metaavg, "metamax":metamax, "worst":0, "worststart":0, "worstend":0};
	return timer;
}

function MakeTimerCid(id, name, group, cid, average, max, min, exclaverage, exclmax, callaverage, callcount, total, meta, metaavg, metamax)
{
	var spike = max <= 0 || average <= 0 ? 0 : (100 * max / average);
	var timer = {"id":id, "name":name, "len":name.length, "cid":cid, "timercid":cid, "group":group, "average":average, "max":max, "min":min, "exclaverage":exclaverage, "exclmax":exclmax, "callaverage":callaverage, "callcount":callcount, "spike":spike,"total":total, "meta":meta, "metaavg":metaavg, "metamax":metamax, "worst":0, "worststart":0, "worstend":0};
	return timer;
}

function CloneTimer(T)
{
	return MakeTimerCid(T.id, T.name, T.group, T.cid, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
function CloneGroup(G)
{
	return MakeGroupCid(G.id, G.name, G.category, G.numtimers, G.isgpu, G.total, G.average, G.max, G.cid);
}

function MakeFrame(id, framestart, frameend, framestartgpu, frameendgpu, ts, tt, ti)
{
	var frame = {"id":id, "framestart":framestart, "frameend":frameend, "framestartgpu":framestartgpu, "frameendgpu":frameendgpu, "ts":ts, "tt":tt, "ti":ti};
	return frame;
}
function MakeCounterHistory(id, history, prc)
{
	var counterhistory = {"id":id, "history":history, "prc":prc};
	return counterhistory;
}
function MakeCounter(id, parent, sibling, firstchild, level, name, value, minvalue, maxvalue, formatted, limit, formattedlimit, counterprc, boxprc, format, counterhistory)
{
	var counter = { "id":id, "parent":parent, "sibling":sibling, "firstchild":firstchild, "level": level, "name":name, "value":value, "formatted":formatted, "limit":limit, "formattedlimit":formattedlimit, "counterprc":counterprc, "boxprc":boxprc, "counterhistory":counterhistory, "format":format, "minvalue":minvalue, "maxvalue":maxvalue};
	return counter;
}
function Clamp(v, low, high)
{
	return v < low ? low : (v > high ? high : v);
}
function FormatTime(Time)
{
	return Time.toFixed(2);
}



var g_ColorMap = {};
var g_Colors = new Array();
var g_ColorsDark = new Array();
var g_ColorsTextIndex = new Array();
var g_ColorH = new Array();
var g_ColorS = new Array();
var g_ColorL = new Array();

function GetColorIndex(color)
{
	if(g_ColorMap[color])
	{
		return g_ColorMap[color];
	}
	else
	{
		var idx = g_Colors.length;
		g_ColorMap[color] = idx;
		g_Colors.push(color);
		g_ColorsDark.push(ConvertColorDark(color));
		g_ColorsTextIndex.push(InvertColorIndex(color));
		var hsl = ConvertRgbToHsl(color);
		g_ColorH.push(hsl[0]);
		g_ColorS.push(hsl[1]);
		g_ColorL.push(hsl[2]);
		return idx;
	}
}
//color index 0 reserved for blinking
var cidhovercolor = 0;
g_Colors.push('#000000');
g_ColorsDark.push('#000000');
g_ColorsTextIndex.push(0);
g_ColorH.push(0);
g_ColorS.push(0);
g_ColorL.push(0);

var S = {};
var S2 = {};
var Timeline = {};
S.TimelineArray = [];
S.TimelineIdArray = [];
S.TimelineNames = [];
S.TimelineColorArray = [];



//EBEGIN
____embed____
//EEND

var G_DEBUG = 0;

var CanvasDetailedView = document.getElementById('DetailedView');
var CanvasHistory = document.getElementById('History');
var CanvasMenu = document.getElementById('CanvasMenu');
var CanvasDetailedOffscreen = document.createElement('canvas');
var FilterInputGroup = document.getElementById('filtergroup');
var FilterInputTimer = document.getElementById('filtertimer');

var FilterInput = document.getElementById('FilterInput');
var FilterInputDiv = document.getElementById('FilterInputDiv');
var FilterInputDivPos = {"x":-1,"y":-1,"w":-1,"h":-1};
var FilterInputMenu = document.getElementById('FilterInputMenu');
var FilterInputMenuDiv = document.getElementById('FilterInputMenuDiv');
var FilterInputMenuDivPos = {"x":-1,"y":-1,"w":-1,"h":-1};
var FilterInputMenuValueLast = '';


var FilterInputGroupString = null;
var FilterInputTimerString = null;
var FilterInputArray = [FilterInputGroup, FilterInputTimer];
var FilterGroup = null;
var FilterTimer = null;
var g_Msg = '0';

var RedrawRequested = 0;
var InsideDraw = 0;



var Initialized = 0;
var fDetailedOffset = S.Frames[0].framestart;
var fDetailedOffsetSecond = 0;
var fDetailedRange = S.Frames[S.Frames.length-1].frameend - fDetailedOffset;
var nWidth = CanvasDetailedView.width;
var nHeight = CanvasDetailedView.height;
var ReferenceTimeString = 'auto';
var ReferenceTimeAutoString = 'auto';
var ReferenceTime = 33;
var TargetTime = 16;
var TargetTimeString = '16ms';
var nHistoryHeight = 70;
var nOffsetY = 0;
var nOffsetBarsX = 0;
var nOffsetBarsY = 0;
var nOffsetCountersY = 0;
var nOffsetFilterSearch = 0;
var nOffsetMenuThreads = 0;
var nOffsetMenuGroups = 0;
var FilterInputSearchLast = '';
var nBarsWidth = 80;
var MouseButtonState = [0,0,0,0,0,0,0,0];
var KeyShiftDown = 0;
var KeyAltDown = 0;
var KeyCtrlDown = 0;
var MouseDragButton = 0;
var ToolTipFlip = 0; 
var ToolTipCorner = 1;
var DetailedViewMouseX = 0;
var DetailedViewMouseY = 0;
var DetailedMouseOverButton = 0;
var DetailedMouseEvent = null;
var HistoryViewMouseX = -1;
var HistoryViewMouseY = -1;
var GlobalMouseX = -1;
var GlobalMouseY = -1;
var MouseReleased = false;
var MouseHistory = 0;
var MouseDetailed = 0;
var FontHeight = 10;
var FontWidth = 1;
var FontAscent = 3; //Set manually
var Font = 'Bold ' + FontHeight + 'px Courier New';
var FontFlash = 'Bold ' + 35 + 'px Courier New';
var BoxHeight = FontHeight + 2;
var ThreadsActive = {};
var GroupsDisabled = new Object();
var nMinWidth = 0.01;//subpixel width
var nMinWidthPan = 1.0;//subpixel width when panning
var nContextSwitchEnabled = 1;
var DisableLod = 0;
var DisableMerge = 0;
var GroupColors = 0;
var DrawDetailedFlameMode = 0;
var DrawDetailedCompareReverse = 1;
var nModDown = 0;
var g_MSG = 'no';
var nDrawCount = 0;
var nBackColors = ['#292929', '#343434' ];
var nBackColorsDark = ['#292929', '#272727' ];
var nBackColorOffset = '#404040';
var CSwitchColors =["#9DD8AF","#D7B6DA","#EAAC76","#DBDA61","#8AD5E1","#8CE48B","#C4D688","#57E5C4"];//generated by http://tools.medialab.sciences-po.fr/iwanthue/index.php
var CSwitchHeight = 5;
var FRAME_HISTORY_COLOR_CPU = '#ff7f27';
var FRAME_HISTORY_COLOR_GPU = '#ffffff';
var ZOOM_TIME = 0.5;
var AnimationActive = false;
var nHoverCSCpu = -1;
var nHoverCSCpuNext = -1;
var nHoverCSToolTip = null;
var nHoverToken = -1;
var HoverTokenOwner = null;
var nHoverFrame = -1;
var nHoverTokenIndex = -1;
var nHoverTokenLogIndex = -1;
var nHoverCounter = 0;
var nHoverCounterDelta = 8;
var nHoverTokenNext = -1;
var nHoverTokenLogIndexNext = -1;
var nHoverTokenIndexNext = -1;
var HoverTokenNextOwner = null;
var nHoverCounter = -1;
var nHoverTokenDrawn = -1;
var nHideHelp = 0;
var fFrameScale = 33.33;
var SortColumn = 0;
var SortColumnOrderFlip = 0;
var SortColumnMouseOver = null;
var SortColumnMouseOverNext = null;
var ColumnsWidth = [];
var ColumnsEnabled = [];
let ColumnNames =["Average", "Max", "Total", "Min", "Spike", "Call Average", "Call Count", "Excl Average", "Excl Max"];
let NumColumns = ColumnNames.length;
while(ColumnsEnabled.length < NumColumns)
{
	ColumnsEnabled.push(1);
	ColumnsWidth.push(20);
}
var FilterSearchActive = 0;
var FilterSearchSelection = -1;
var FilterSearchSelectionMax = 0;
var FilterSearchPassIndex = -1;
FilterSearchReset();
var FilterSearchStartTime = new Date();
var IgnoreInput = 0;
function RangeInit()
{
	return {"Begin":-1, "End":-1, "YBegin":-1, "YEnd":-1, "Thread": -1 , "Index": -1, "Off": 0, "Second":0};
}
function RangeValid(Range)
{
	return Range.Begin < Range.End;
}
function RangeCopy(Dst, Src)
{
	Dst.Begin = Src.Begin;
	Dst.End = Src.End;
	Dst.YBegin = Src.YBegin;
	Dst.YEnd = Src.YEnd;
	Dst.Thread = Src.Thread;
	Dst.Off = Src.Off;
	Dst.Second = Src.Second;
}
var RangeCpu = RangeInit();
var RangeGpu = RangeInit();
var RangeSelect = RangeInit();

var RangeCpuNext = RangeInit();
var RangeGpuNext = RangeInit();

var RangeCpuHistory = RangeInit();
var RangeGpuHistory = RangeInit();

var fRangeBegin = 0;
var fRangeEnd = -1;
var fRangeThreadId = -1;
var fRangeThreadIdNext = -1;
var fRangeBeginNext = 0;
var fRangeEndNext = 0;
var fRangeBeginGpuNext = 0;
var fRangeEndGpuNext = 0;
var fRangeBeginHistory = -1;
var fRangeEndHistory = -1;
var fRangeBeginHistoryGpu = -1;
var fRangeEndHistoryGpu = -1;
var fRangeBeginSelect = 0;
var fRangeEndSelect = -1;
var ThreadYBegin;
var ThreadYEnd;

var ModeDetailed = 0;
var ModeTimers = 1;
var ModeTimers_Threads = 2;
var ModeTimers_Groups = 3;
var ModeCounters = 4;
var ModeCount = 5;
var Mode = ModeDetailed;

var DebugDrawQuadCount = 0;
var DebugDrawTextCount = 0;
var ProfileMode = 0;
var ProfileRedraw0 = 0;
var ProfileRedraw1 = 0;
var ProfileRedraw2 = 0;
var ProfileFps = 0;
var ProfileFpsAggr = 0;
var ProfileFpsCount = 0;
var ProfileLastTimeStamp = new Date();

var ProfileData = {};
var ProfileStackTime = {};
var ProfileStackName = {};
var Debug = 1;

var ThreadLogAutoHide = 0;
var NumLodSplits = 10;
var SplitMin = 100;
var SPLIT_LIMIT = 1e20;
var DPR = 1;
var DetailedRedrawState = {};
var OffscreenData;
var DetailedFrameCounter = 0;
var Invalidate = 0;
var GroupOrder = Array();
var ThreadOrder = Array();
var ThreadOrderNames = Array();
var ThreadOrderT = Array();
var MetaLengths = Array();
var MetaLengthsAvg = Array();
var MetaLengthsMax = Array();

var ZoomActive = 0;

var StrGroup = "Group";
var StrThread = "Thread";
var StrTimer = "Timer";
var StrAverage = "Average";
var StrMax = "Max";
var StrTotal = "Total";
var StrMin = "Min";
var StrSpike = "Spike%";
var StrCallAverage = "Call Average";
var StrCount = "Count";
var StrExclAverage = "Excl Average";
var StrExclMax = "Excl Max";

var GroupRemapReverse = [];
var TimerRemapReverse = [];
var ThreadRemapReverse = [];

let SubMenuHelp = 0;
let SubMenuMode = 1;
let SubMenuReference = 2;
let SubMenuTarget = 3;
let SubMenuThreads = 4;
let SubMenuGroups = 5;
let SubMenuColumns = 6;
let SubMenuOptions = 7;
let SubMenuCompare = 8;

let MenuRedraw = 0;
let SubMenuActive = -1;
let SubMenuTimeoutBase = 0.7;
let SubMenuMouseX = 0;
let SubMenuMouseY = 0;
let SubMenuTimeout = new Date();
let MenuItems = [];
let FilterInputMenuThreadsValue = '';
let FilterInputMenuGroupsValue = '';
let MouseMoveTime = new Date();
let ReferenceTimes = [-1, 5, 10, 15, 16, 20, 30, 33, 50, 100, 250, 500, 1000];
let TargetTimes = [5, 10, 15, 16, 20, 30, 33, 50, 100, 250, 500, 1000];
let ModeItems = ["Detailed", "Timers", "Threads", "Groups", "Counters"];
let HideModeCollapsed = 0;
let HideModeHidden = 1;
let HideMode = HideModeCollapsed;


function ProfileModeClear()
{
	if(ProfileMode)
	{
		ProfileData = new Object();
		ProfileStackTime = new Array();
		ProfileStackName = new Array();
	}
}
function ProfileEnter(Name)
{
	if(ProfileMode)
	{
		ProfileStackTime.push(new Date());
		ProfileStackName.push(Name);
	}
}
function ProfileLeave()
{
	if(ProfileMode)
	{
		var Time = new Date();
		var Delta = Time - ProfileStackTime.pop();
		var Name = ProfileStackName.pop();
		var Obj = ProfileData[Name];
		if(!Obj)
		{
			Obj = new Object();
			Obj.Count = 0;
			Obj.Name = Name;
			Obj.Time = 0;
			ProfileData[Name] = Obj;
		}
		Obj.Time += Delta;
		Obj.Count += 1;
	}
}

function ProfilePlot(s)
{
	if(ProfileMode)
	{
		var A = ProfileData.Plot;
		if(!A)
		{
			ProfileData.Plot = Array();
			A = ProfileData.Plot;
		}
		if(A.length<10)
		{
			A.push(s);
		}
	}
}
function ProfileModeDump()
{
	for(var idx in ProfileData)
	{
		var Timer = ProfileData[idx];
		console.log(Timer.Name + " " + Timer.Time + "ms " + Timer.Count);
	}

}
function ProfileModeDraw(Canvas)
{
	if(ProfileMode)
	{
		var StringArray = [];
		for(var idx in ProfileData)
		{
			if(idx == "Plot")
				continue;
			var Timer = ProfileData[idx];
			StringArray.push(Timer.Name);
			StringArray.push(Timer.Time + "ms");
			StringArray.push("#");
			StringArray.push("" + Timer.Count);
		}
		StringArray.push("debug");
		StringArray.push(Debug);
		var Time = new Date();
		var Delta = Time - ProfileLastTimeStamp;
		ProfileLastTimeStamp = Time;
		StringArray.push("Frame Delta");
		StringArray.push(Delta + "ms");
		if(ProfileMode == 2)
		{
			ProfileFpsAggr += Delta;
			ProfileFpsCount ++ ;
			var AggrFrames = 10;
			if(ProfileFpsCount == AggrFrames)
			{
				ProfileFps = 1000 / (ProfileFpsAggr / AggrFrames);
				ProfileFpsAggr = 0;
				ProfileFpsCount = 0;
			}
			StringArray.push("FPS");
			StringArray.push("" + ProfileFps.toFixed(2));
		}
		StringArray.push("ProfileRedraw0");
		StringArray.push("" + ProfileRedraw0);
		StringArray.push("ProfileRedraw1");
		StringArray.push("" + ProfileRedraw1);
		StringArray.push("ProfileRedraw2");
		StringArray.push("" + ProfileRedraw2);
		ProfileRedraw0 = 0;
		ProfileRedraw1 = 0;
		ProfileRedraw2 = 0;


		for(var i = 0; i < ProfileData.Plot; ++i)
		{
			StringArray.push("");
			StringArray.push(ProfileData.Plot[i]);
		}
		ProfileData.Plot = Array();
		DrawToolTip(StringArray, Canvas, 0, 200);
	}
}

function ToggleDebugMode()
{
	ProfileMode = (ProfileMode+1)%4;
	console.log('Toggle Debug Mode ' + ProfileMode);
}

function DetailedTotal(S)
{
	var Total = 0;
	for(var i = 0; i < S.Frames.length; i++)
	{
		var frfr = S.Frames[i];
		Total += frfr.frameend - frfr.framestart;
	}
	return Total;
}


function InitFrameInfo()
{

	var div = document.getElementById('divFrameInfo');
	var txt = '';
	txt = txt + 'Timers View' + '<br>';
	txt = txt + 'Frames:' + S.AggregateInfo.Frames +'<br>';
	txt = txt + 'Time:' + S.AggregateInfo.Time.toFixed(2) +'ms<br>';
	txt = txt + '<hr>';
	txt = txt + 'Detailed View' + '<br>';
	txt = txt + 'Frames:' + S.Frames.length +'<br>';
	txt = txt + 'Time:' + DetailedTotal(S).toFixed(2) +'ms<br>';
	div.innerHTML = txt;
}
function InitGroups()
{
	for(groupid in S.GroupInfo)
	{
		var TimerArray = Array();
		for(timerid in S.TimerInfo)
		{
			if(S.TimerInfo[timerid].group == groupid)
			{
				TimerArray.push(timerid);
			}
		}
		S.GroupInfo[groupid].TimerArray = TimerArray;
	}
}


function ThreadOrderMove(Name, Dir, Top)
{	
	let idx = ThreadOrderNames.indexOf(Name);
	let base = idx;
	idx += Dir;
	let found = -1;
	while(idx >= 0 && idx <= ThreadOrderNames.length)
	{
		let n = S.ThreadNames.indexOf(ThreadOrderNames[idx]);
		if(n != -1)
		{
			found = idx;
			if(!Top)
				break;
		}
		idx += Dir;
	}
	Dir = -Dir; 
	while(found >= 0 && found <= ThreadOrderNames.length && (Dir > 0 ? (found <= base) : (found >= base)))
	{
		let Tmp = ThreadOrderNames[found];
		ThreadOrderNames[found] = Name;
		Name = Tmp;
		found += Dir;
	}
	ThreadOrderSort();
}
function ThreadOrderMoveUp(Name, Top)
{
	ThreadOrderMove(Name, -1, Top);
	WriteCookie();
}
function ThreadOrderMoveDown(Name, Top)
{
	ThreadOrderMove(Name, 1, Top);
	WriteCookie();
}
function ThreadOrderSort()
{
	for(let i = 0; i < S.ThreadNames.length; ++i)
	{
		var name = S.ThreadNames[i];
		var idx = ThreadOrderNames.indexOf(name);
		if(idx == -1)
		{
			ThreadOrderNames.push(name);
		}
	}

	//clear all that are not present??
	let ThreadOrderX = Array(S.ThreadNames.length);
	for(let i = 0; i < S.ThreadNames.length; ++i)
	{
		var n = ThreadOrderNames.indexOf(S.ThreadNames[i]);
		if(n == -1)
		{
			debugger;
		}
		ThreadOrderX[i] = {"i":i, "n": n};
	}
	ThreadOrderX.sort(function(l,r){return l.n-r.n;});
	ThreadOrderT = Array(S.ThreadNames.length);
	for(let i = 0; i < S.ThreadNames.length; ++i)
	{
		ThreadOrderT[i] = ThreadOrderX[i].i;
	}
}

function ToggleThread(ThreadName, All, FilterArray, State)
{
	if(ThreadName)
	{
		if(ThreadsActive[ThreadName])
		{
			ThreadsActive[ThreadName] = false;
		}
		else
		{
			ThreadsActive[ThreadName] = true;
		}
	}
	else
	{
		let Names = All ? S.ThreadNames : FilterArray;

		for(let i in Names)
		{
			switch(State)
			{
				case 2: if(!ThreadsActive[Names[i]]){ ThreadsActive[Names[i]] = 1;} break;
				case 1: ThreadsActive[Names[i]] = ThreadsActive[Names[i]] ? 0 : 1; break;
				case 0: if(ThreadsActive[Names[i]]){ ThreadsActive[Names[i]] = 0;} break;
			}
		}
	}
	RequestRedraw();
	Invalidate = 0;
	FilterSearchReset();
	WriteCookie();
}


function CreateOrderArray(Source, NameFunc)
{
	var Temp = Array(Source.length);
	for(var i = 0; i < Source.length; ++i)
	{
		Temp[i] = {};
		Temp[i].index = i;
		Temp[i].namezz = NameFunc(Source[i]).toLowerCase();
	}
	Temp.sort(function(l, r)
	{ 
		if(r.namezz<l.namezz)
			{return 1;}
		if(l.namezz<r.namezz)
			{return -1;} 
		return 0;
	} );
	var OrderArray = Array(Source.length);
	for(var i = 0; i < Source.length; ++i)
	{
		OrderArray[i] = Temp[i].index;
	}
	return OrderArray;
}

function InitOrderArrays()
{
	ThreadOrder = [];
	ThreadOrder = CreateOrderArray(S.ThreadNames, function(a){return a;});


	let MaxLen = 7;
	let MenuArray = Array();
	for(let i = 0; i < S.GroupInfo.length; ++i)
	{
		let x = {};
		x.IsCategory = 0;
		x.category = S.GroupInfo[i].category;
		x.name = S.GroupInfo[i].name;
		x.index = i;
		MenuArray.push(x);
	}
	for(let i = 0; i < S.CategoryInfo.length; ++i)
	{
		let x = {};
		x.IsCategory = 1;
		x.category = i;
		x.name = S.CategoryInfo[i];
		x.index = i;
		MenuArray.push(x);
	}
	let OrderFunction = function(a){ return a.category + "__" + a.name; };
	let OrderFunctionMenu = function(a){ return a.IsCategory ? (a.category + '') : (a.category + "__" + a.name); };
	GroupOrder = CreateOrderArray(S.GroupInfo, OrderFunction);
}

function CategoryIndex(CategoryName)
{
	for(var i = 0; i < S.CategoryInfo.length; ++i)
	{
		if(S.CategoryInfo[i] == CategoryName)
		{
			return i;
		}
	}
	return -1;
}
function IsCategoryActive(CategoryIdx)
{
	for(var i = 0; i < S.GroupInfo.length; ++i)
	{
		if(S.GroupInfo[i].category == CategoryIdx)
		{
			var Name = S.GroupInfo[i].name;
			if(!GroupsActive[Name])
			{
				return false;
			}
		}
	}
	return true;

}
function ToggleCategory(CategoryName, WantRedraw)
{
	var CategoryIdx = CategoryIndex(CategoryName);
	if(CategoryIdx < 0)
		return;
	var CategoryActive = IsCategoryActive(CategoryIdx);
	for(var i = 0; i < S.GroupInfo.length; ++i)
	{
		if(S.GroupInfo[i].category == CategoryIdx)
		{
			var Name = S.GroupInfo[i].name;
			if(CategoryActive)
			{
				delete GroupsDisabled[Name];
			}
			else
			{
				GroupsDisabled[Name] = 1;
			}
		}
	}
	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}

function ToggleGroup(GroupName, All, FilterArray, State)
{
	if(GroupName)
	{
		if(GroupsDisabled[GroupName])
		{
			delete GroupsDisabled[GroupName];
		}
		else
		{
			GroupsDisabled[GroupName] = 1;
		}
	}
	else
	{
		let Names = FilterArray;
		if(All)
		{
			Names = [];
			for(let i in S.GroupInfo)
			{
				Names.push(S.GroupInfo[i].name);
			}
		}

		for(let i in Names)
		{
			let N = Names[i];
			switch(State)
			{
				case 2: if(GroupsDisabled[N]){ delete GroupsDisabled[N];} break;
				case 1: 
					if(GroupsDisabled[N])
					{
						delete GroupsDisabled[N];
					}
					else
					{
						GroupsDisabled[N] = 1;
					}

					break;
				case 0: if(!GroupsDisabled[N]){ GroupsDisabled[N] = 1;} break;
			}
		}
	}
	WriteCookie();
}

function UpdateGroupColors()
{
	for(var i = 0; i < S.TimerInfo.length; ++i)
	{
		if(GroupColors == 1)
		{
			S.TimerInfo[i].cid = S.GroupInfo[S.TimerInfo[i].group].cid;
		}
		else
		{
			S.TimerInfo[i].cid = S.TimerInfo[i].timercid;
		}
	}
}
function ToggleDetailedFlameMode(WantRedraw)
{
	DrawDetailedFlameMode = (DrawDetailedFlameMode+1)%3;
	if(WantRedraw)
	{
		RequestRedraw();
	}
}	

function ToggleDetailedSecondReverse(WantRedraw)
{
	DrawDetailedCompareReverse = 1-DrawDetailedCompareReverse;
	if(WantRedraw)
	{
		RequestRedraw();
	}
}	

function ToggleGroupColors(WantRedraw)
{
	GroupColors = (GroupColors+1)%3;
	UpdateGroupColors();
	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}

function ShowHelp(Show, Forever)
{
	var HelpWindow = document.getElementById('helpwindow');
	if(Show)
	{
		HelpWindow.style['display'] = 'block';
	}
	else
	{
		HelpWindow.style['display'] = 'none';
	}
	if(Forever)
	{
		nHideHelp = Show ? 0 : 1;
		WriteCookie();
	}
}
function ToggleMode()
{
	Mode = (Mode + 1) % ModeCount;
	SetMode(Mode);
}

function SetMode(NewMode, WantRedraw)
{
	ResetColumnWidth();
	Mode = NewMode;
	if(Mode == ModeTimers || Mode == ModeTimers_Groups || Mode == ModeTimers_Threads)
	{
		SetFilterInput(FilterInputGroupString, FilterInputTimerString);
	}
	else
	{
		ShowFilterInput(0);
	}

	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}


function UpdateReferenceTime()
{
	if('auto' == ReferenceTimeString.substring(0,4))
	{
		let Max = 0.1;
		for(var i = 0; i < S.Frames.length; ++i)
		{
			let T = S.Frames[i].frameend - S.Frames[i].framestart;
			Max = Math.max(T, Max);
		}
		ReferenceTime = Max*1.20;
		if(TargetTime>0)
		{
			ReferenceTime = Math.max(ReferenceTime, TargetTime * 1.33);
		}
		ReferenceTimeAutoString = 'auto [' + Max.toFixed(2) + 'ms]';
	}
	else
	{
		ReferenceTime = parseInt(ReferenceTimeString);
	}
}

function SetReferenceTime(TimeString, WantRedraw)
{
	ReferenceTimeString = TimeString;
	UpdateReferenceTime();
	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}

function SetTargetTime(TimeString, WantRedraw)
{
	TargetTimeString = TimeString;
	TargetTime = parseInt(TargetTimeString);
	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}

function ToggleContextSwitch()
{
	SetContextSwitch(nContextSwitchEnabled ? 0 : 1);
}

function SetContextSwitch(Enabled, WantRedraw)
{
	nContextSwitchEnabled = Enabled ? 1 : 0;
	WriteCookie();
	if(WantRedraw)
	{
		RequestRedraw();
	}
}

function ToggleDebug()
{
	Debug = (Debug + 1) % 2;
}

function ToggleDisableMerge()
{
	DisableMerge = DisableMerge ? 0 : 1;
}
function ToggleDisableLod()
{
	DisableLod = DisableLod ? 0 : 1;
}

function GatherHoverMetaCounters(TimerIndex, StartIndex, nLog, nFrameLast)
{
	var HoverInfo = new Object();
	return HoverInfo;
	var StackPos = 1;
	//search backwards, count meta counters 
	for(var i = nFrameLast; i >= 0; i--)
	{
		var fr = S.Frames[i];
		var ts = fr.ts[nLog];
		var ti = fr.ti[nLog];
		var tt = fr.tt[nLog];
		var start = i == nFrameLast ? StartIndex-1-fr.LogStart[nLog] : ts.length-1;

		for(var j = start; j >= 0; j--)
		{
			var type = tt[j];
			var index = ti[j];
			var time = ts[j];
			if(type == 1)
			{
				StackPos--;
				if(StackPos == 0 && index == TimerIndex)
				{
					return HoverInfo;
				}
			}
			else if(type == 0)
			{
				StackPos++;
			}
			else if(type > 1)
			{
				// var nMetaCount = type - 3;
				// var nMetaIndex = MetaNames[index];
				// if(nMetaIndex in HoverInfo)
				// {
				// 	HoverInfo[nMetaIndex] += nMetaCount;
				// }
				// else
				// {
				// 	HoverInfo[nMetaIndex] = nMetaCount;
				// }
			}
		}
	}
}
function CalculateAllTimers(fBegin, fEnd)
{
	var Sum = [];
	var Count = [];
	var Sorted = [];
	for(var i = 0; i < S.TimerInfo.length; ++i)
	{
		Sum.push(0.0);
		Count.push(0);
		Sorted.push(i);
	}
	var nFrameFirst = 0;
	var nFrameLast = S.Frames.length;

	var nNumLogs = S.Frames[0].ts.length;
	var StackPosArray = Array(nNumLogs);
	var StackArray = Array(nNumLogs);
	for(var i = 0; i < nNumLogs; ++i)
	{
		StackPosArray[i] = 0;
		StackArray[i] = Array(20);
	}

	for(var i = nFrameFirst; i < nFrameLast; i++)
	{
		var fr = S.Frames[i];
		for(nLog = 0; nLog < nNumLogs; nLog++)
		{
			var StackPos = StackPosArray[nLog];
			var Stack = StackArray[nLog];
			var ts = fr.ts[nLog];
			var ti = fr.ti[nLog];
			var tt = fr.tt[nLog];
			var count = ts.length;
			for(j = 0; j < count; j++)
			{
				var type = tt[j];
				var index = ti[j];
				var time = ts[j];
				if(type == 1 && time < fEnd) //enter
				{
					Stack[StackPos] = time < fBegin ? fBegin : time;
					if(StackArray[nLog][StackPos] != time)
					{
						console.log('fail fail fail');
					}
					StackPos++;
				}
				else if(type == 0) // leave
				{
					if(StackPos>0)
					{
						var timeend = time;
						StackPos--;
						timestart = Stack[StackPos];
						var TimeDelta = timeend - timestart;
						Sum[index] += TimeDelta;
						Count[index]++;
					}
				}
			}
			StackPosArray[nLog] = StackPos;
		}
	}
	Sorted.sort(function(a,b){ return Sum[b] - Sum[a]; } );
	var Result = {"Sorted" : Sorted, "Sum" : Sum, "Count" : Count};
	return Result;
}

function CalculateTimers2(Result, TimerIndex, Time, SContext)
{
	let nFrame = -1;
	for(var i = 0; i < SContext.Frames.length; ++i)
	{
		let F = SContext.Frames[i];
		if(Time > F.framestart && Time <= F.frameend)
		{
			nFrame = i;
			break;
		}
	}
	CalculateTimers(Result, TimerIndex, nFrame, nFrame+1, SContext);
	return nFrame;
}

function CalculateTimers(Result, TimerIndex, nFrameFirst, nFrameLast, SContext)
{
	if(!SContext)
		SContext = S;
	if(!nFrameFirst || nFrameFirst < 0)
		nFrameFirst = 0;
	if(!nFrameLast || nFrameLast > SContext.Frames.length)
		nFrameLast = SContext.Frames.length;
	var FrameCount = nFrameLast - nFrameFirst;
	if(0 == FrameCount)
		return;
	var CallCount = 0;
	var Sum = 0;
	var Max = 0;
	var FrameMax = 0;

	var nNumLogs = SContext.Frames[0].ts.length;
	var StackPosArray = Array(nNumLogs);
	var StackArray = Array(nNumLogs);
	let StackIndexArray = Array(nNumLogs);
	for(var i = 0; i < nNumLogs; ++i)
	{
		StackPosArray[i] = 0;
		StackArray[i] = Array(32);
		StackIndexArray[i] = Array(32);
	}

	for(var i = nFrameFirst; i < nFrameLast; i++)
	{
		let FrameSum = 0;
		let fr = SContext.Frames[i];
		for(nLog = 0; nLog < nNumLogs; nLog++)
		{
			let StackPos = StackPosArray[nLog];
			let Stack = StackArray[nLog];
			let StackIndex = StackIndexArray[nLog];
			var ts = fr.ts[nLog];
			var ti = fr.ti[nLog];
			var tt = fr.tt[nLog];
			var count = ts.length;
			for(var j = 0; j < count; j++)
			{
				var type = tt[j];
				var index = ti[j];
				var time = ts[j];
				if(type == 1) //enter
				{
					//push
					Stack[StackPos] = time;
					StackIndex[StackPos] = index;
					if(StackArray[nLog][StackPos] != time)
					{
						console.log('fail fail fail');
					}
					StackPos++;
				}
				else if(type == 0) // leave
				{
					var timestart;
					var timeend = time;
					if(StackPos>0)
					{
						StackPos--;
						timestart = Stack[StackPos];
					}
					else
					{
						timestart = SContext.Frames[nFrameFirst].framestart;
					}
					if(index == TimerIndex)
					{
						let TimeDelta = timeend - timestart;
						CallCount++;
						FrameSum += TimeDelta;
						Sum += TimeDelta;
						if(TimeDelta > Max)
							Max = TimeDelta;
					}
				}
				else
				{
					//meta
				}
			}
			if(i == nFrameLast - 1)
			{
				for(var j = 0; j < StackPos; ++j)
				{
					if(StackIndex[j] == TimerIndex)
					{
						let LastFrameEnd = SContext.Frames[nFrameLast-1].frameend;
						let TimeDelta = LastFrameEnd - Stack[j];
						CallCount++;
						FrameSum += TimeDelta;
						Sum += TimeDelta;
						if(TimeDelta > Max)
							Max = TimeDelta;
						break;
					}
				}

			}
			if(FrameSum > FrameMax)
			{
				FrameMax = FrameSum;
			}
			StackPosArray[nLog] = StackPos;
		}
	}

	Result.CallCount = CallCount;
	Result.Sum = Sum.toFixed(3);
	Result.Max = Max.toFixed(3);
	Result.Average = (Sum / CallCount).toFixed(3);
	Result.FrameAverage = (Sum / FrameCount).toFixed(3);
	Result.FrameCallAverage = (CallCount / FrameCount).toFixed(3);
	Result.FrameMax = FrameMax.toFixed(3);
	return Result;
}

function PreprocessCalculateAllTimers()
{
	ProfileEnter("CalculateAllTimers");
	var nFrameFirst = 0;
	var nFrameLast = S.Frames.length;
	var FrameCount = nFrameLast - nFrameFirst;
	if(0 == FrameCount)
		return;
	for(var j = 0; j < S.TimerInfo.length; j++)
	{
		S.TimerInfo[j].CallCount = 0;
		S.TimerInfo[j].Sum = 0;
		S.TimerInfo[j].Max = 0;
		S.TimerInfo[j].FrameMax = 0;
	}


	var nNumLogs = S.Frames[0].ts.length;
	var StackPosArray = Array(nNumLogs);
	var StackArray = Array(nNumLogs);
	for(var i = 0; i < nNumLogs; ++i)
	{
		StackPosArray[i] = 0;
		StackArray[i] = Array(20);
	}

	for(var i = nFrameFirst; i < nFrameLast; i++)
	{
		for(var j = 0; j < S.TimerInfo.length; j++)
		{
			S.TimerInfo[j].FrameSum = 0;
		}

		var fr = S.Frames[i];
		for(nLog = 0; nLog < nNumLogs; nLog++)
		{
			var StackPos = StackPosArray[nLog];
			var Stack = StackArray[nLog];
			var ts = fr.ts[nLog];
			var ti = fr.ti[nLog];
			var tt = fr.tt[nLog];
			var count = ts.length;
			for(j = 0; j < count; j++)
			{
				var type = tt[j];
				var index = ti[j];
				var time = ts[j];
				if(type == 1) //enter
				{
					//push
					Stack[StackPos] = time;
					if(StackArray[nLog][StackPos] != time)
					{
						console.log('fail fail fail');
					}
					StackPos++;
				}
				else if(type == 0) // leave
				{
					var timestart;
					var timeend = time;
					if(StackPos>0)
					{
						StackPos--;
						timestart = Stack[StackPos];
					}
					else
					{
						timestart = S.Frames[nFrameFirst].framestart;
					}
					// if(index == TimerIndex)
					{
						var TimeDelta = timeend - timestart;
						S.TimerInfo[index].CallCount++;
						S.TimerInfo[index].FrameSum += TimeDelta;
						S.TimerInfo[index].Sum += TimeDelta;
						if(TimeDelta > S.TimerInfo[index].Max)
						{
							S.TimerInfo[index].Max = TimeDelta;
							S.TimerInfo[index].worst = TimeDelta;
							S.TimerInfo[index].worststart = timestart;
							S.TimerInfo[index].worstend = timeend;
							S.TimerInfo[index].worstthread = nLog;
						}
					}
				}
				else
				{
					//meta
				}
			}
			for(var j = 0; j < S.TimerInfo.length; j++)
			{
				if(S.TimerInfo[j].FrameSum > S.TimerInfo[j].FrameMax)
				{
					S.TimerInfo[j].FrameMax = S.TimerInfo[j].FrameSum;
				}
			}
			StackPosArray[nLog] = StackPos;
		}


	}

	for(var j = 0; j < S.TimerInfo.length; j++)
	{
		var CallCount = S.TimerInfo[j].CallCount;
		var Sum = S.TimerInfo[j].Sum.toFixed(3);
		var Max = S.TimerInfo[j].Max.toFixed(3);
		var Average = (S.TimerInfo[j].Sum / S.TimerInfo[j].CallCount).toFixed(3);
		var FrameAverage = (S.TimerInfo[j].Sum / FrameCount).toFixed(3);
		var FrameCallAverage = (S.TimerInfo[j].CallCount / FrameCount).toFixed(3);
		var FrameMax = S.TimerInfo[j].FrameMax.toFixed(3);
		S.TimerInfo[j].CallCount = CallCount;
		S.TimerInfo[j].Sum = Sum;
		S.TimerInfo[j].Max  = Max ;
		S.TimerInfo[j].Average = Average;
		S.TimerInfo[j].FrameAverage = FrameAverage;
		S.TimerInfo[j].FrameCallAverage = FrameCallAverage;
		S.TimerInfo[j].FrameMax = FrameMax;
	}
	ProfileLeave();
}

var FlashFrames = 10;
var FlashFrameCounter = 0;
var FlashMessage = '';
function TimeString(Diff)
{
	var DiffString = "0 sec";
	var DiffTable = [1,60,60*60,60*60*24];
	var DiffNameTable = ["sec", "min", "hr", "day"];
	for(var i = 0; i < DiffTable.length; ++i)
	{
		if(Diff >= DiffTable[i])
		{
			DiffString = Math.floor(Diff / DiffTable[i]) + " " + DiffNameTable[i];
		}
	}
	return DiffString;

}
function ShowFlashMessage(Message, FrameCount)
{
	FlashMessage = Message;
	FlashFrameCounter = FrameCount;
}
function OnPageReady()
{
	var DumpDate = S.DumpUtcCaptureTime;
	var CurrentDate = Date.now() / 1000;
	var Diff = CurrentDate - DumpDate;
	var Limit = 10*60;//flash old message when loading captures older than 10 minutes 
	if(Diff > Limit)
	{
		ShowFlashMessage("Captured " + TimeString(Diff) + " ago", 100);
	}
}

function DrawFlashMessage(context)
{
	if(FlashFrameCounter > 0)
	{
		if(FlashFrameCounter>1)
		{
			var FlashPrc = Math.sin(FlashFrameCounter / FlashFrames);
			context.font = FontFlash;
			context.globalAlpha = FlashPrc * 0.35 + 0.5;
			context.textAlign = 'center';
			context.fillStyle = 'red';
			context.fillText(FlashMessage, nWidth * 0.5, 50);
			context.globalAlpha = 1;
			context.textAlign = 'left';
			context.font = Font;
		}
		FlashFrameCounter -= 1;

	}
}

function DrawCaptureInfo(context)
{
	context.fillStyle = 'white';
	context.textAlign = 'right';
	context.font = Font;
	var DumpDate = S.DumpUtcCaptureTime;
	var CurrentDate = Date.now() / 1000;
	var Diff = CurrentDate - DumpDate;
	var DiffString = TimeString(Diff) + " ago";
	context.fillText(new Date(DumpDate*1000).toLocaleString(), nWidth, FontHeight);
	if(Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
	{
		context.fillText("Timer Frames: " + S.AggregateInfo.Frames, nWidth, FontHeight*2);
	}
	else
	{
		context.fillText("Detailed Frames "+ S.Frames.length, nWidth, FontHeight*2);
	}
	context.fillText(S.DumpHost, nWidth, FontHeight*3);
	context.fillText(DiffString, nWidth, FontHeight*4);
	context.textAlign = 'left';
	DrawFlashMessage(context);
}

function DrawDetailedFrameHistory()
{
	ProfileEnter("DrawDetailedFrameHistory");
	var x = HistoryViewMouseX;

	var context = CanvasHistory.getContext('2d');
	context.clearRect(0, 0, CanvasHistory.width, CanvasHistory.height);

	let HistoryHeight = CanvasHistory.height;

	var GreenTime = (TargetTime * 0.9);
	var RedBegin = (TargetTime * 1.1);
	var LerpDist = 1.0 / (RedBegin - GreenTime);


	var fHeight = nHistoryHeight;
	var fWidth = nWidth / S.Frames.length;
	var fHeightScale = fHeight / ReferenceTime;
	var fX = 0;
	var FrameIndex = -1;
	var MouseDragging = MouseDragState != MouseDragOff;
	RangeCpuHistory = RangeInit();
	RangeGpuHistory = RangeInit()

	var FrameFirst = -1;
	var FrameLast = nWidth;
	var fDetailedOffsetEnd = fDetailedOffset + fDetailedRange;
	for(i = 0; i < S.Frames.length; i++)
	{
		var fMs = S.Frames[i].frameend - S.Frames[i].framestart;

		var fPrc = (fMs - GreenTime) * LerpDist;
		fPrc = Clamp(fPrc, 0, 1);
		var color = LerpColor(fPrc);



		if(fDetailedOffset <= S.Frames[i].frameend && fDetailedOffset >= S.Frames[i].framestart)
		{
			var lerp = (fDetailedOffset - S.Frames[i].framestart) / (S.Frames[i].frameend - S.Frames[i].framestart);
			FrameFirst = fX + fWidth * lerp;
		}
		if(fDetailedOffsetEnd <= S.Frames[i].frameend && fDetailedOffsetEnd >= S.Frames[i].framestart)
		{
			var lerp = (fDetailedOffsetEnd - S.Frames[i].framestart) / (S.Frames[i].frameend - S.Frames[i].framestart);
			FrameLast = fX + fWidth * lerp;
		}
		var fH = fHeightScale * fMs;
		var bMouse = x > fX && x < fX + fWidth;
		if(bMouse && !MouseDragging)
		{
			context.fillStyle = FRAME_HISTORY_COLOR_GPU;
			RangeCpuHistory.Begin = S.Frames[i].framestart;
			RangeCpuHistory.End = S.Frames[i].frameend;
			if(S.Frames[i].framestartgpu)
			{
				RangeGpuHistory.Begin = S.Frames[i].framestartgpu;
				RangeGpuHistory.End = S.Frames[i].frameendgpu;
			}
			FrameIndex = i;
		}
		else
		{
			context.fillStyle = color;
		}
		context.fillRect(fX, fHeight - fH, fWidth-1, fH);
		fX += fWidth;
	}

	var fRangeHistoryBegin = FrameFirst;
	var fRangeHistoryEnd = FrameLast;
	var X = fRangeHistoryBegin;
	var Y = 0;
	var W = fRangeHistoryEnd - fRangeHistoryBegin;
	context.globalAlpha = 0.35;
	context.fillStyle = '#009900';
	context.fillRect(X, Y, W, fHeight);
	context.globalAlpha = 1;
	context.strokeStyle = '#00ff00';
	context.beginPath();
	context.moveTo(X, Y);
	context.lineTo(X, Y+fHeight);
	context.moveTo(X+W, Y);
	context.lineTo(X+W, Y+fHeight);
	context.stroke();

	{
		var fH = fHeight - fHeightScale * TargetTime;
		context.fillStyle = 'wheat';
		context.strokeStyle = 'wheat';
		context.beginPath();
		context.moveTo(0, fH);
		context.lineTo(nWidth, fH);
		// context.closePath();
		context.stroke();
		var YText;
		if(fH > HistoryHeight * 0.25)
		{
			YText = fH - FontAscent;
		}
		else
		{
			YText = fH + FontHeight;
		}
		context.fillText(TargetTime + 'ms', 3, YText);
		context.textAlign='right';
		context.fillText(FormatTime(ReferenceTime) + 'ms', nWidth, FontHeight);
		context.textAlign='left';
	}


	DrawCaptureInfo(context);

	if(FrameIndex>=0 && !MouseDragging)
	{
		var StringArray = [];
		StringArray.push("Frame");
		StringArray.push("" + FrameIndex);
		StringArray.push("Time");
		StringArray.push("" + (S.Frames[FrameIndex].frameend - S.Frames[FrameIndex].framestart).toFixed(3));

		DrawToolTip(StringArray, CanvasHistory, HistoryViewMouseX, HistoryViewMouseY+20);

	}
	ProfileLeave();
}
function TimeToMsString(Time)
{
	return Time.toFixed(3) + "ms";
}
function TimeToString(Time)
{
	if(Time > 1000)
	{
		return (Time/1000.0).toFixed(0) +"s";
	}
	else if(Time > 0.9)
	{
		return Time.toFixed(0) + "ms";
	}
	else if(Time > 0.0009)
	{
		return (Time*1000).toFixed(0) + "us";
	}
	else
	{
		return (Time*1000000).toFixed(0) + "ns";
	}
}

function DrawDetailedBackground(context)
{
	var fMs = fDetailedRange;
	var fMsEnd = fMs + fDetailedOffset;
	var fMsToScreen = nWidth / fMs;
	var fRate = Math.floor(2*((Math.log(fMs)/Math.log(10))-1))/2;
	var fStep = Math.pow(10, fRate);
	var fRcpStep = 1.0 / fStep;
	var nColorIndex = Math.floor(fDetailedOffset * fRcpStep) % 2;
	if(nColorIndex < 0)
		nColorIndex = -nColorIndex;
	var fStart = Math.floor(fDetailedOffset * fRcpStep) * fStep;
	var fHeight = CanvasDetailedView.height;
	var fScaleX = nWidth / fDetailedRange; 
	var HeaderString = TimeToString(fStep);
	context.textAlign = 'center';
	for(f = fStart; f < fMsEnd; )
	{
		var fNext = f + fStep;
		var X = (f - fDetailedOffset) * fScaleX;
		var W = (fNext-f)*fScaleX;
		context.fillStyle = nBackColors[nColorIndex];
		context.fillRect(X, 0, W+2, fHeight);
		nColorIndex = 1 - nColorIndex;
		context.fillStyle = '#777777'
		context.fillText(HeaderString, X + W * 0.5, 10);
		context.fillText(HeaderString, X + W * 0.5, nHeight - 10);
		f = fNext;
	}
	context.textAlign = 'left';
	var fScaleX = nWidth / fDetailedRange; 
	context.globalAlpha = 0.5;
	context.strokeStyle = '#bbbbbb';
	context.beginPath();
	for(var i = 0; i < S.Frames.length; i++)
	{
		var frfr = S.Frames[i];
		if(frfr.frameend < fDetailedOffset || frfr.framestart > fDetailedOffset + fDetailedRange)
		{
			continue;
		}
		var X = (frfr.framestart - fDetailedOffset) * fScaleX;
		if(X >= 0 && X < nWidth)
		{
			context.moveTo(X, 0);
			context.lineTo(X, nHeight);
		}
	}
	context.stroke();
	context.globalAlpha = 1;

}

function DrawToolTip(StringArray, Canvas, x, y, bSecondary)
{
	var context = Canvas.getContext('2d');
	context.font = Font;
	var WidthArray = Array(StringArray.length);
	var nMaxWidth = 0;
	var nHeight = 0;
	for(i = 0; i < StringArray.length; i += 2)
	{
		var nWidth0 = context.measureText(StringArray[i]).width;
		var nWidth1 = context.measureText(StringArray[i+1]).width;
		var nSum = nWidth0 + nWidth1;
		WidthArray[i] = nWidth0;
		WidthArray[i+1] = nWidth1;
		if(nSum > nMaxWidth)
		{
			nMaxWidth = nSum;
		}
		nHeight += BoxHeight;
	}
	nMaxWidth += 15;
	//bounds check.
	if(!bSecondary)
	{
		var CanvasRect = Canvas.getBoundingClientRect();
		if(y + nHeight > CanvasRect.height)
		{
			y = CanvasRect.height - nHeight;
			x += 20;
		}
		if(x + nMaxWidth > CanvasRect.width)
		{
			x = CanvasRect.width - nMaxWidth;
		}
	}
	else
	{
		x -= nMaxWidth;
	}
	context.fillStyle = 'black';
	context.fillRect(x-1, y, nMaxWidth+2, nHeight);
	context.fillStyle = 'white';

	var XPos = x;
	var XPosRight = x + nMaxWidth;
	var YPos = y + BoxHeight-2;
	for(i = 0; i < StringArray.length; i += 2)
	{
		context.fillText(StringArray[i], XPos, YPos);
		context.fillText(StringArray[i+1], XPosRight - WidthArray[i+1], YPos);
		YPos += BoxHeight;
	}
	return {"x":x, "y":y};
}

function BuildToolTipDetailed(S, nHoverToken, nHoverFrame, HoverTokenOwner, bSecond)
{
	var StringArray = [];
	var groupid = S.TimerInfo[nHoverToken].group;
	if(nHoverToken > -1)
	{
		StringArray.push("Timer");
		StringArray.push(S.TimerInfo[nHoverToken].name);
		StringArray.push("Group");
		StringArray.push(S.GroupInfo[groupid].name);
	}
	else
	{
		StringArray.push("");
		StringArray.push("");
		StringArray.push("");
		StringArray.push("");	
	}
	StringArray.push("");
	StringArray.push("");

	if(S == HoverTokenOwner)
	{
		StringArray.push("Time");
		StringArray.push((RangeCpu.End-RangeCpu.Begin).toFixed(3));
	}
	else
	{
		StringArray.push("");
		StringArray.push("");
	}

	StringArray.push("");
	StringArray.push("");
	StringArray.push("Total");
	StringArray.push("" + S.TimerInfo[nHoverToken].Sum);
	StringArray.push("Max");
	StringArray.push("" + S.TimerInfo[nHoverToken].Max);
	StringArray.push("Average");
	StringArray.push("" + S.TimerInfo[nHoverToken].Average);
	StringArray.push("Count");
	StringArray.push("" + S.TimerInfo[nHoverToken].CallCount);

	StringArray.push("");
	StringArray.push("");

	StringArray.push("Max/Frame");
	StringArray.push("" + S.TimerInfo[nHoverToken].FrameMax);

	StringArray.push("Average Time/Frame");
	StringArray.push("" + S.TimerInfo[nHoverToken].FrameAverage);

	StringArray.push("Average Count/Frame");
	StringArray.push("" + S.TimerInfo[nHoverToken].FrameCallAverage);


	let Time = fDetailedOffset + fDetailedRange * (DetailedViewMouseX / nWidth);
	if(bSecond)
		Time += fDetailedOffsetSecond;
	let FrameTime = new Object();
	let Frame = CalculateTimers2(FrameTime, nHoverToken, Time, S);
	StringArray.push("");
	StringArray.push("");
	if(Frame>-1)
	{
		StringArray.push("Frame " + Frame);
		StringArray.push("");
		StringArray.push("Total");
		StringArray.push("" + FrameTime.Sum);
		StringArray.push("Count");
		StringArray.push("" + FrameTime.CallCount);
		StringArray.push("Average");
		StringArray.push("" + FrameTime.Average);
		StringArray.push("Max");
		StringArray.push("" + FrameTime.Max);
	}
	else
	{
		for(var i = 0; i < 10; ++i)
			StringArray.push("");
	}

	StringArray.push("");
	StringArray.push("");

	StringArray.push("Detailed Capture");
	StringArray.push("");
	StringArray.push("Frames");
	StringArray.push(S.Frames.length);
	StringArray.push("Time");
	StringArray.push(DetailedTotal(S).toFixed(2) + "ms");	
	return StringArray;



} 


function DrawHoverToolTip()
{
	ProfileEnter("DrawHoverToolTip");
	var StringArray = [];
	let StringArray2;
	if(nHoverToken != -1)
	{

		var bShowTimers = Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups;
		// this confused alot of people.
		// if(ToolTipFlip == 1)
		// {
		// 	bShowTimers = !bShowTimers;
		// }
		if(bShowTimers)
		{
			// var StringArray = [];
			var groupid = S.TimerInfo[nHoverToken].group;
			StringArray.push("Timer");
			StringArray.push(S.TimerInfo[nHoverToken].name);
			StringArray.push("Group");
			StringArray.push(S.GroupInfo[groupid].name);

			StringArray.push("");
			StringArray.push("");
			var Timer = S.TimerInfo[nHoverToken];
			StringArray.push("Average");
			StringArray.push(Timer.average);
			StringArray.push("Max");
			StringArray.push(Timer.max);
			StringArray.push("Excl Max");
			StringArray.push(Timer.exclmax);
			StringArray.push("Excl Average");
			StringArray.push(Timer.exclaverage);
			StringArray.push("Call Average");
			StringArray.push(Timer.callaverage);
			StringArray.push("Call Count");
			StringArray.push(Timer.callcount);

			StringArray.push("");
			StringArray.push("");


			StringArray.push("Group");
			StringArray.push(S.GroupInfo[groupid].name);
			StringArray.push("Average");
			StringArray.push(S.GroupInfo[groupid].average);
			StringArray.push("Max");
			StringArray.push(S.GroupInfo[groupid].max);

			StringArray.push("");
			StringArray.push("");

			StringArray.push("Timer Capture");
			StringArray.push("");
			StringArray.push("Frames");
			StringArray.push(S.AggregateInfo.Frames);
			StringArray.push("Time");
			StringArray.push(S.AggregateInfo.Time.toFixed(2) + "ms");




		}
		else
		{
			StringArray = BuildToolTipDetailed(S, nHoverToken, nHoverFrame, HoverTokenOwner, 0);
			let nCompareHoverToken = TimerRemapReverse[nHoverToken];
			if(nCompareHoverToken > -1)
			{
				StringArray2 = BuildToolTipDetailed(S2, nCompareHoverToken, null, HoverTokenOwner, 1);
			}
		}
		if(ToolTipCorner)
		{
			let R = DrawToolTip(StringArray, CanvasDetailedView, nWidth, nHeight);
			if(StringArray2)
				DrawToolTip(StringArray2, CanvasDetailedView, R.x-15, R.y, 1);
		}
		else
		{
			let R = DrawToolTip(StringArray, CanvasDetailedView, DetailedViewMouseX, DetailedViewMouseY+20);
			if(StringArray2)
				DrawToolTip(StringArray2, CanvasDetailedView, R.x-15, R.y, 1);
		}
	}
	else if(nHoverCSCpu >= 0)
	{
		var StringArray = [];
		StringArray.push("Context Switch");
		StringArray.push("");
		StringArray.push("");
		StringArray.push("");
		StringArray.push("Cpu");
		StringArray.push("" + nHoverCSCpu);
		StringArray.push("Begin");
		StringArray.push("" + RangeCpu.Begin);
		StringArray.push("End");
		StringArray.push("" + RangeCpu.End);
		if(ToolTipCorner)
		{
			DrawToolTip(StringArray, CanvasDetailedView, nWidth, nHeight);
		}
		else
			DrawToolTip(StringArray, CanvasDetailedView, DetailedViewMouseX, DetailedViewMouseY+20);
	}
	ProfileLeave();
}

function FormatMeta(Value, Dec)
{
	if(!Value)
	{
		Value = "0";
	}
	else
	{
		Value = '' + Value.toFixed(Dec);
	}
	return Value;
}

function FilterMatch(FilterArray, value)
{
	if(!FilterArray)
		return true;
	for(var i = 0; i < FilterArray.length; ++i)
	{
		var res = value.search(FilterArray[i]);
		if(res<0)
			return false;
	}
	return true;
}


function DrawBarView()
{
	ProfileEnter("DrawBarView");
	Invalidate++;
	nHoverToken = -1;
	nHoverFrame = -1;
	var context = CanvasDetailedView.getContext('2d');
	context.clearRect(0, 0, nWidth, nHeight);

	var Height = BoxHeight;
	var Width = nWidth;
	var NameWidth = Math.max(S.TimerNameWidth, S.GroupNameWidth) + 20;

	//clamp offset to prevent scrolling into the void
	var nTotalRows = 0;
	for(var groupid in S.GroupInfo)
	{
		if(!GroupsDisabled[S.GroupInfo[groupid].name])
		{
			nTotalRows += S.GroupInfo[groupid].TimerArray.length + 1;
		}
	}
	var nTotalRowPixels = nTotalRows * Height;
	var nFrameRows = nHeight - BoxHeight;

	if(nOffsetBarsY + nFrameRows > nTotalRowPixels && nTotalRowPixels > nFrameRows)
	{
		nOffsetBarsY = nTotalRowPixels - nFrameRows;
	}
	var ColumnsWidthBefore = new Array(ColumnsWidth.length);
	for(var i = 0; i < ColumnsWidth.length; ++i)
	{
		ColumnsWidthBefore[i] = ColumnsWidth[i];
	}

	var Y = -nOffsetBarsY + BoxHeight;
	let TimersGroups = Mode == ModeTimers_Threads || Mode == ModeTimers_Groups;
	if(TimersGroups)
	{
		nOffsetBarsX = 0;
	}
	var XBase = -nOffsetBarsX;
	var nColorIndex = 0;

	context.fillStyle = 'white';
	context.font = Font;
	var bMouseIn = 0;
	var RcpReferenceTime = 1.0 / ReferenceTime;
	var CountWidth = 12 * FontWidth;
	var nMetaLen = S.TimerInfo[0].meta.length;
	var nMetaCharacters = 10;
	var InnerBoxHeight = BoxHeight-2;
	var TimerLen = 8; //todo: fix max digits.
	var TimerWidth = TimerLen * FontWidth;
	var nWidthBars = nBarsWidth+2;
	var nWidthMs = TimerWidth+2+10;
	var R = 0;
	var AllColumns = TimersGroups != 0;



	for(var i = 0; i < nMetaLen; ++i)
	{
		if(nMetaCharacters < MetaNames[i].length)
			nMetaCharacters = MetaNames[i].length;
	}
	var nWidthMeta = nMetaCharacters * FontWidth + 6;
	function HeaderMouseHandle(XBegin, X, Header)
	{
		var bMouseIn = DetailedViewMouseY >= 0 && DetailedViewMouseY < BoxHeight && DetailedViewMouseX < X && DetailedViewMouseX > XBegin;
		if(bMouseIn)
		{
			SortColumnMouseOverNext = Header;
		}
	}
	function HeaderString(Header)
	{
		if(Header == SortColumnMouseOver)
		{
			return Header + (SortColumnOrderFlip ? '<' : '>');
		}
		else
		{
			return Header;
		}

	}
	function DrawHeaderSplit(Header)
	{
		if(ColumnsEnabled[R]||AllColumns)
		{
			context.fillStyle = 'white';
			context.fillText(HeaderString(Header), X, Height-FontAscent);
			var XBegin = X;
			X += nWidthBars;
			context.fillStyle = nBackColorOffset;
			X += ColumnsWidth[R];
			if(X >= NameWidth)
			{
				context.fillRect(X-3, 0, 1, nHeight);
			}
			HeaderMouseHandle(XBegin, X, Header);
		}
		R++;
	}
	function DrawHeaderSplitSingle(Header, Col)
	{
		if(ColumnsEnabled[Col]||AllColumns)
		{
			context.fillStyle = 'white';
			context.fillText(HeaderString(Header), X, Height-FontAscent);
			var XBegin = X;
			X += ColumnsWidth[R];
			context.fillStyle = nBackColorOffset;
			if(X >= NameWidth)
			{
				context.fillRect(X-3, 0, 1, nHeight);
			}
			HeaderMouseHandle(XBegin, X, Header);
		}
		R++;
	}
	function DrawHeaderSplitLeftRight(HeaderLeft, HeaderRight, Width)
	{
		context.textAlign = 'left';
		context.fillStyle = 'white';
		context.fillText(HeaderLeft, X, Height-FontAscent);
		var XBegin = X;
		X += Width;
		context.textAlign = 'right';
		context.fillText(HeaderRight, X-5, Height-FontAscent);
		context.textAlign = 'left';
		context.fillStyle = nBackColorOffset;
		if(X >= NameWidth)
		{
			context.fillRect(X-3, 0, 1, nHeight);
		}
		HeaderMouseHandle(XBegin, X, HeaderLeft);
	}
	function DrawTimer(Value, Color)
	{
		if(ColumnsEnabled[R]||AllColumns)
		{
			var Prc = Value * RcpReferenceTime;
			var YText = Y+Height-FontAscent;
			if(Prc > 1)
			{
				Prc = 1;
			}
			context.textAlign = 'left';
			context.fillStyle = Color;
			context.fillRect(X+1, Y+1, Prc * nBarsWidth, InnerBoxHeight);
			var TimerText = Value.toFixed(2);
			var W = context.measureText(TimerText).width + FontWidth;
			ColumnsWidth[R] = Math.max(W, ColumnsWidth[R]);
			X += nWidthBars;
			X += ColumnsWidth[R];
			context.fillStyle = 'white';
			context.textAlign = 'right';
			context.fillText(TimerText, X - FontWidth, YText);
			context.textAlign = 'left';
		}
		R++;
	}
	function DrawCount(Str)
	{
		if(ColumnsEnabled[R]||AllColumns)
		{
			X += ColumnsWidth[R];
			context.fillStyle = 'white';
			context.textAlign = 'right';
			var YText = Y+Height-FontAscent;
			context.fillText(Str, X-6, YText);
			var W = Math.max(80, context.measureText(Str).width + FontWidth * 2);
			ColumnsWidth[R] = Math.max(W, ColumnsWidth[R]);
		}
		R++;
	
	}


	function DrawMeta(Value, Width, Dec, YText, Col)
	{
		if(ColumnsEnabled[Col]||AllColumns)
		{
			Value = FormatMeta(Value, Dec);
			X += (FontWidth*Width);
			ColumnsWidth[R] = FontWidth*Width;
			context.textAlign = 'right';
			context.fillText(Value, X-FontWidth, YText);
			context.textAlign = 'left';
		}
		R++;
	}



	function DrawTimerRow(timerid, showgroup)
	{
		R = 0;
		var Timer = S.TimerInfo[timerid];
		var Average = Timer.average;
		var Max = Timer.max;
		var Min = Timer.min;
		var Spike = Timer.spike;
		var ExclusiveMax = Timer.exclmax;
		var ExclusiveAverage = Timer.exclaverage;
		var CallAverage = Timer.callaverage;
		var CallCount = Timer.callcount;
		var YText = Y+Height-FontAscent;
		var Color = g_Colors[Timer.cid];
		X = NameWidth + XBase;

		nColorIndex = 1-nColorIndex;
		bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + BoxHeight;
		if(bMouseIn)
		{
			nHoverToken = timerid;
		}
		context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillRect(0, Y, Width, FontHeight+2);

		DrawTimer(Average,  Color);
		DrawTimer(Max, Color);
		DrawTimer(Timer.total, Color);
		DrawTimer(Min, Color);
		DrawCount(Spike.toFixed(2) + '%');
		DrawTimer(CallAverage, Color);
		DrawCount(CallCount);
		DrawTimer(ExclusiveAverage, Color);
		DrawTimer(ExclusiveMax, Color);

		context.fillStyle = 'white';
		var Col = R;
		for(var j = 0; j < nMetaLen; ++j)
		{
		    DrawMeta(Timer.meta[j], MetaLengths[j], 0, YText, Col + j);
		    DrawMeta(Timer.metaavg[j], MetaLengthsAvg[j], 2, YText, Col + j);
		    DrawMeta(Timer.metamax[j], MetaLengthsMax[j], 0, YText, Col + j);
		}
		context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillRect(0, Y, NameWidth, Height);
		context.textAlign = 'right';
		context.fillStyle = Color;
		context.fillText(Timer.name, NameWidth - 5, YText);
		context.textAlign = 'left';
		if(showgroup)
		{
			context.fillStyle = 'white';
			context.fillText(S.GroupInfo[Timer.group].name, 1, YText);
		}
	}
	if(SortColumn && Mode == ModeTimers)
	{
		var OrderArray = new Array(S.TimerInfo.length);
		var KeyArray = new Array(S.TimerInfo.length);
		for(var idx in GroupOrder)
		{
			var Group = S.GroupInfo[idx];
			if((!GroupsDisabled[Group.name]) && FilterMatch(FilterGroup, Group.name))
			{
				var TimerArray = Group.TimerArray;
				for(var timerindex in TimerArray)
				{
					var timerid = TimerArray[timerindex];
					if(FilterMatch(FilterTimer, S.TimerInfo[timerid].name))
					{
						OrderArray.push(timerid);
						NameWidth = Math.max(S.TimerInfo[timerid].wtotal, NameWidth);
					}
				}
			}
		}
		var KeyFunc = null;
		switch(SortColumn)
		{
			case 1: KeyFunc = function (a) { return S.TimerInfo[a].average; }; break;
			case 2: KeyFunc = function (a) { return S.TimerInfo[a].max; }; break;
			case 3: KeyFunc = function (a) { return S.TimerInfo[a].total; }; break;
			case 4: KeyFunc = function (a) { return S.TimerInfo[a].min; }; break;
			case 5: KeyFunc = function (a) { return S.TimerInfo[a].spike; }; break;
			case 6: KeyFunc = function (a) { return S.TimerInfo[a].callaverage; }; break;
			case 7: KeyFunc = function (a) { return S.TimerInfo[a].callcount; }; break;
			case 8: KeyFunc = function (a) { return S.TimerInfo[a].exclaverage; }; break;
			case 9: KeyFunc = function (a) { return S.TimerInfo[a].exclmax; }; break;
		}

		var Flip = SortColumnOrderFlip == 1 ? -1 : 1;
		OrderArray.sort(function(a,b) { return Flip * (KeyFunc(b) - KeyFunc(a)); } );

		for(var i in OrderArray)
		{
			DrawTimerRow(OrderArray[i], 1);
			Y += Height;
		}			

	}
	else if(Mode == ModeTimers_Threads)
	{
		for(var i = 0; i < S.ThreadNames.length; ++i)
		{
			if((ThreadsActive[S.ThreadNames[i]]) && FilterMatch(FilterTimer, S.ThreadNames[i]))
			{
				var X = 0;
				var YText = Y+Height-FontAscent;
				bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + BoxHeight;
				nColorIndex = 1-nColorIndex;
				context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
				context.fillRect(0, Y, Width, FontHeight+2);
				var ThreadColor = CSwitchColors[i % CSwitchColors.length];
				context.fillStyle = ThreadColor;
				context.fillText(S.ThreadNames[i], 1, YText);
				context.textAlign = 'left';
				Y += Height;
				for(var idx in GroupOrder)
				{
					R = 0;
					var groupid = GroupOrder[idx];
					var Group = S.GroupInfo[groupid];
					var PerThreadTimer = S.ThreadGroupTimeArray[i][groupid];
					var PerThreadTimerTotal = S.ThreadGroupTimeTotalArray[i][groupid];
					if((PerThreadTimer > 0.0001|| PerThreadTimerTotal>0.1) && (!GroupsDisabled[Group.name]) && FilterMatch(FilterGroup, Group.name))
					{
						var GColor = GroupColors ? g_Colors[S.GroupInfo[groupid].cid] : 'white';
						var X = 0;
						nColorIndex = 1-nColorIndex;
						bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + BoxHeight;
						context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
						context.fillRect(0, Y, Width, nHeight);
						context.fillStyle = GColor;
						context.textAlign = 'right';
						context.fillText(Group.name, NameWidth - 5, Y+Height-FontAscent);
						context.textAlign = 'left';
						X += NameWidth;
						DrawTimer(PerThreadTimer, GColor);
						DrawTimer(PerThreadTimerTotal, GColor);
						Y += Height;
					}
				}
			}
		}
	}
	else
	{
		for(var idx in GroupOrder)
		{
			var groupid = GroupOrder[idx];
			var Group = S.GroupInfo[groupid];
			var GColor = GroupColors ? g_Colors[S.GroupInfo[groupid].cid] : 'white';
			if((!GroupsDisabled[Group.name]) && FilterMatch(FilterGroup, Group.name))
			{
				R = 0;
				var TimerArray = Group.TimerArray;
				var X = XBase;
				nColorIndex = 1-nColorIndex;
				bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + BoxHeight;
				context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
				context.fillRect(0, Y, Width, FontHeight+2);
				context.fillStyle = GColor;
				context.fillText(Group.name, 1, Y+Height-FontAscent);
				X += NameWidth;
				DrawTimer(Group.average, GColor);
				DrawTimer(Group.max, GColor);
				DrawTimer(Group.total, GColor);

				context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
				context.fillRect(0, Y, NameWidth, FontHeight+2);
				context.fillStyle = GColor;
				context.fillText(Group.name, 1, Y+Height-FontAscent);

				Y += Height;
				let TimersGroups = Mode == ModeTimers_Threads || Mode == ModeTimers_Groups;
				if(TimersGroups)
				{
					for(var i = 0; i < S.ThreadNames.length; ++i)
					{
						R = 0;
						var PerThreadTimer = S.ThreadGroupTimeArray[i][groupid];
						var PerThreadTimerTotal = S.ThreadGroupTimeTotalArray[i][groupid];
						if((PerThreadTimer > 0.0001|| PerThreadTimerTotal>0.1) && (ThreadsActive[S.ThreadNames[i]]) && FilterMatch(FilterTimer, S.ThreadNames[i]))
						{
							var YText = Y+Height-FontAscent;
							bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + BoxHeight;
							nColorIndex = 1-nColorIndex;
							context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
							context.fillRect(0, Y, Width, FontHeight+2);
							var ThreadColor = CSwitchColors[i % CSwitchColors.length];
							context.fillStyle = ThreadColor;
							context.textAlign = 'right';
							context.fillText(S.ThreadNames[i], NameWidth - 5, YText);
							context.textAlign = 'left';
							X = NameWidth;
							DrawTimer(PerThreadTimer, ThreadColor);
							X += nWidthBars + ColumnsWidth[R++];	
							DrawTimer(PerThreadTimerTotal, ThreadColor);
							Y += Height;
						}
					}
				}
				else
				{
					for(var timerindex in TimerArray)
					{
						var timerid = TimerArray[timerindex];
						if(FilterMatch(FilterTimer, S.TimerInfo[timerid].name))
						{
							DrawTimerRow(timerid, 0);
							Y += Height;
						}
					}			
				}
			}
		}
	}
	X = 0;
	R = 0;
	context.fillStyle = nBackColorOffset;
	context.fillRect(0, 0, Width, Height);
	context.fillStyle = 'white';
	SortColumnMouseOverNext = null;

	if(Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
	{
		if(Mode == ModeTimers_Threads)
		{
			DrawHeaderSplitLeftRight(StrThread, StrGroup, NameWidth);
			DrawHeaderSplit(StrAverage);
			DrawHeaderSplit(StrTotal);
		}
		else
		{
			DrawHeaderSplitLeftRight(StrGroup, StrThread, NameWidth);
			DrawHeaderSplit(StrAverage);
			DrawHeaderSplit(StrMax);
			DrawHeaderSplit(StrTotal);
		}
	}
	else
	{
		X = NameWidth + XBase;
		DrawHeaderSplit(StrAverage);
		DrawHeaderSplit(StrMax);
		DrawHeaderSplit(StrTotal);
		DrawHeaderSplit(StrMin);
		DrawHeaderSplitSingle(StrSpike, R);
		DrawHeaderSplit(StrCallAverage);
		DrawHeaderSplitSingle(StrCount, R);
		DrawHeaderSplit(StrExclAverage);
		DrawHeaderSplit(StrExclMax);
		var Col = R;
		for(var i = 0; i < nMetaLen; ++i)
		{
			DrawHeaderSplitSingle(MetaNames[i], Col + i);
			DrawHeaderSplitSingle(MetaNames[i] + " Avg", Col + i);
			DrawHeaderSplitSingle(MetaNames[i] + " Max", Col + i);
		}
		X = 0;
		context.fillStyle = nBackColorOffset;
		context.fillRect(0, 0, NameWidth, Height);
		context.fillStyle = 'white';
	
		DrawHeaderSplitLeftRight(StrGroup, StrTimer, NameWidth);
	
	}

	var ColumnsChanged = false;
	for(var i = 0; i < ColumnsWidth.length; ++i)
	{
		if(ColumnsWidthBefore[i] != ColumnsWidth[i])
		{
			ColumnsChanged = true;
		}
	}
	if(ColumnsChanged)
	{
		Invalidate = 0;
	}




	ProfileLeave();
}

var CounterNameWidth = 100;
var CounterValueWidth = 100;
var CounterLimitWidth = 100;

var FormatCounterDefault = 0;
var FormatCounterBytes = 1;
var FormatCounterBytesExt = [ "b","kb","mb","gb","tb","pb", "eb","zb", "yb" ];

function ShiftRight10(v)
{
	if(v > 1024)
	{
		return v / 1024.0;
	}
	else
	{
		return v >> 10;
	}
}


function FormatCounter(Format, Counter)
{
	if(!Counter)
	{
		return '0';
	}
	var Negative = 0;
	if(Counter < 0)
	{
		Counter = -Counter;
		Negative = 1;
		if(Counter < 0) // handle INT_MIN
		{
			Counter = -(Counter+1);
			if(Counter < 0)
			{
				return '?';
			}
		}
	}
	var str = Negative ? '-' :'' ;
	if(Format == FormatCounterDefault)
	{
		var Seperate = 0;
		var result = '';
		while (Counter)
		{
			if (Seperate)
			{
				result += '.';
			}
			Seperate = 1;
			for (var i = 0; Counter && i < 3; ++i)
			{
				var Digit = Math.floor(Counter % 10);
				Counter = Math.floor(Counter / 10);
				result += '' + Digit;
			}
		}

		for(var i = 0; i < result.length; ++i)
		{
			str += result[result.length-1-i];
		}
		return str;
	}
	else if(Format == FormatCounterBytes)
	{
		var Shift = 0;
		var Divisor = 1;
		var CountShifted = ShiftRight10(Counter);
		while(CountShifted)
		{
			Divisor <<= 10;
			CountShifted = ShiftRight10(CountShifted);
			Shift++;
		}
		if(Shift)
		{
			return str + (Counter / Divisor).toFixed(2) + '' + FormatCounterBytesExt[Shift];
		}
		else
		{
			return str + Counter.toFixed(2) + '' + FormatCounterBytesExt[0];
		}
	}
	return '?'
}
function DrawCounterView()
{
	ProfileEnter("DrawCounterView");
	Invalidate++;
	nHoverToken = -1;
	nHoverFrame = -1;
	var context = CanvasDetailedView.getContext('2d');
	context.clearRect(0, 0, nWidth, nHeight);

	var Height = BoxHeight;
	var Width = nWidth;
	//clamp offset to prevent scrolling into the void
	var nTotalRows = S.CounterInfo.length;
	var nTotalRowPixels = nTotalRows * Height;
	var nFrameRows = nHeight - BoxHeight;
	if(nOffsetCountersY + nFrameRows > nTotalRowPixels && nTotalRowPixels > nFrameRows)
	{
		nOffsetCountersY = nTotalRowPixels - nFrameRows;
	}

	var CounterNameWidthTemp = 10;
	var CounterValueWidthTemp = 10;
	var CounterLimitWidthTemp = 10;

	var CounterWidth = 150;
	var Y = -nOffsetCountersY + BoxHeight;
	var X = 0;
	var nColorIndex = 0;
	context.fillStyle = 'white';
	context.font = Font;
	var bMouseIn = 0;
	function DrawHeaderSplitSingle(Header, Width)
	{
		context.fillStyle = 'white';
		context.fillText(Header, X, Height-FontAscent);
		X += Width;
		context.fillStyle = nBackColorOffset;
		context.fillRect(X-3, 0, 1, nHeight);
	}
	function DrawHeaderSplitSingleRight(Header, Width)
	{
		X += Width;
		context.fillStyle = 'white';
		context.textAlign  = 'right';
		context.fillText(Header, X - FontWidth, Height-FontAscent);
		context.fillStyle = nBackColorOffset;
		context.fillRect(X, 0, 1, nHeight);
		context.textAlign  = 'left';
	}
	var TimerLen = 6;
	var TimerWidth = TimerLen * FontWidth;
	nHoverCounter = -1;
	function CounterIndent(Level)
	{
		return Level * 4 * FontWidth;
	}
	function Max(a, b)
	{
		return a > b ? a : b;
	}

	function DrawCounterRecursive(Index)
	{
		var Counter = S.CounterInfo[Index];
		var Indent = CounterIndent(Counter.level);
		CounterNameWidthTemp = Max(CounterNameWidthTemp, Counter.name.length+1 + Indent / (FontWidth+1));
		CounterValueWidthTemp = Max(CounterValueWidthTemp, Counter.formatted.length);
		CounterLimitWidthTemp = Max(CounterLimitWidthTemp, Counter.formattedlimit.length);

		var X = 0;
		nColorIndex = 1-nColorIndex;
		var HeightExpanded = Counter.Expanded ? Height * 5 : Height

		bMouseIn = DetailedViewMouseY >= Y && DetailedViewMouseY < Y + HeightExpanded;
		if(bMouseIn)
		{
			nHoverCounter = Index;
		}
		var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillStyle = bgcolor;
		context.fillRect(0, Y, Width, HeightExpanded);
		context.fillStyle = 'white';
		var c = Counter.closed ? '*' : ' ';
		context.fillText(c + Counter.name, Indent, Y+Height-FontAscent);
		X += CounterNameWidth;
		X += CounterValueWidth - FontWidth;
		context.textAlign = 'right';
		context.fillText(Counter.formatted, X, Y+Height-FontAscent);
		context.textAlign = 'left';
		X += FontWidth * 4;
		var Y0 = Y + 1;
		if(Counter.limit != 0)
		{
			context.fillText(Counter.formattedlimit, X, Y+Height-FontAscent);
			X += CounterLimitWidth;
			var X0 = X + 1;
			context.fillStyle = 'white';
			context.fillRect(X0, Y0, Counter.boxprc * (CounterWidth-2), Height-2);
			context.fillStyle = bgcolor;
			context.fillRect(X0+1, Y0+1, Counter.boxprc * (CounterWidth-4), Height-4);
			context.fillStyle = 'cyan';
			context.fillRect(X0+1, Y0+1, Counter.counterprc * (CounterWidth-4), Height-4);
			X += CounterWidth + 10;
		}
		else
		{
			X += CounterLimitWidth;
			X += CounterWidth + 10;
		}
		var CounterHistory = Counter.counterhistory;
		if(CounterHistory)
		{
			var Prc = CounterHistory.prc;

			context.fillStyle = 'cyan';
			context.strokeStyle = 'cyan';
			context.globalAlpha = 0.5;
			context.beginPath();
			var x = X;
			var YBase = Y0 + HeightExpanded-1;
			var YOffset = -(HeightExpanded-2);

			context.moveTo(X, Y0);
			for(var i = 0; i < Prc.length; ++i)
			{
				context.moveTo(x, YBase);
				context.lineTo(x, YBase + Prc[i] * YOffset);
				
				x += 1;
			}
			context.stroke();

			x = X;
			context.globalAlpha = 1.0;
			context.beginPath();
			context.moveTo(X, YBase);

			for(var i = 0; i < Prc.length; ++i)
			{
				context.lineTo(x, YBase + Prc[i] * YOffset);
				x += 1;
			}
			context.stroke();
			if(bMouseIn)
			{
				var MouseGraphX = Math.floor(DetailedViewMouseX - X);
				if(MouseGraphX >= 0 && MouseGraphX < CounterHistory.history.length)
				{
					context.fillStyle = 'white';
					var Formatted = FormatCounter(Counter.format, CounterHistory.history[MouseGraphX]);
					context.fillText(Formatted, X, Y+Height-FontAscent);
				}
				context.strokeStyle = 'orange';
				context.beginPath();
				var CrossX = X + MouseGraphX;
				var CrossY = YBase + Prc[MouseGraphX] * YOffset;
				context.moveTo(CrossX-2, CrossY-2);
				context.lineTo(CrossX+2, CrossY+2);
				context.moveTo(CrossX+2, CrossY-2);
				context.lineTo(CrossX-2, CrossY+2);
				context.stroke();

			}
			X += Prc.length + 5;
			context.fillStyle = 'white';
			context.fillText( FormatCounter(Counter.format, Counter.minvalue), X, Y + Height - FontAscent);
			X += CounterWidth + 5;
			context.fillText( FormatCounter(Counter.format, Counter.maxvalue), X, Y + Height - FontAscent);
			X += CounterWidth + 5;
		}

		Y += HeightExpanded;

		if(!Counter.closed)
		{
			var ChildIndex = Counter.firstchild;
			while(ChildIndex != -1)
			{
				DrawCounterRecursive(ChildIndex);
				ChildIndex = S.CounterInfo[ChildIndex].sibling;
			}
		}
	}

	for(var i = 0; i < S.CounterInfo.length; ++i)
	{
		if(S.CounterInfo[i].parent == -1)
		{
			DrawCounterRecursive(i);
		}
	}

	X = 0;
	context.fillStyle = nBackColorOffset;
	context.fillRect(0, 0, Width, Height);
	context.fillStyle = 'white';
	DrawHeaderSplitSingle('Name', CounterNameWidth);
	DrawHeaderSplitSingleRight('Value', CounterValueWidth + (FontWidth+1));
	DrawHeaderSplitSingle('Limit', CounterLimitWidth + CounterWidth + 3 * (FontWidth+1));





	var CounterNameWidthNew = CounterNameWidthTemp * (FontWidth+1);
	var CounterValueWidthNew = CounterValueWidthTemp * (FontWidth+1);
	var CounterLimitWidthNew = CounterLimitWidthTemp * (FontWidth+1);
	if(CounterNameWidthNew != CounterNameWidth || CounterValueWidthNew != CounterValueWidth || CounterLimitWidthNew != CounterLimitWidth)
	{
		// console.log('requesting redraw 0' + CounterNameWidthNew + '= ' + CounterNameWidth );
		// console.log('requesting redraw 1' + CounterValueWidthNew + '= ' + CounterValueWidth );
		// console.log('requesting redraw 2' + CounterLimitWidthNew + '= ' + CounterLimitWidth );
		CounterNameWidth = CounterNameWidthNew;
		CounterValueWidth = CounterValueWidthNew;
		CounterLimitWidth = CounterLimitWidthNew;
		Invalidate = 0;
	}

	ProfileLeave();
}


//preprocess context switch data to contain array per thread
function PreprocessContextSwitchCacheItem(ThreadId)
{
	var CSObject = S.CSwitchCache[ThreadId];
	if(ThreadId > 0 && !CSObject)
	{
		CSArrayIn = new Array();
		CSArrayOut = new Array();
		CSArrayCpu = new Array();
		var nCount = S.CSwitchTime.length;
		var j = 0;
		var TimeIn = -1.0;
		for(var i = 0; i < nCount; ++i)
		{	
			var ThreadIn = S.CSwitchThreadInOutCpu[j];
			var ThreadOut = S.CSwitchThreadInOutCpu[j+1];
			var Cpu = S.CSwitchThreadInOutCpu[j+2];
			if(TimeIn < 0)
			{
				if(ThreadIn == ThreadId)
				{
					TimeIn = S.CSwitchTime[i];
				}
			}
			else
			{
				if(ThreadOut == ThreadId)
				{
					var TimeOut = S.CSwitchTime[i];
					CSArrayIn.push(TimeIn);
					CSArrayOut.push(TimeOut);
					CSArrayCpu.push(Cpu);
					TimeIn = -1;
				}
			}
			j += 3;
		}
		CSObject = new Object();
		CSObject.Size = CSArrayIn.length;
		CSObject.In = CSArrayIn;
		CSObject.Out = CSArrayOut;
		CSObject.Cpu = CSArrayCpu;
		S.CSwitchCache[ThreadId] = CSObject;
	}

}
function PreprocessContextSwitchCache()
{
	ProfileEnter("PreprocessContextSwitchCache");
	var AllThreads = {};
	var nCount = S.CSwitchTime.length;
	for(var i = 0; i < nCount; ++i)
	{	
		var nThreadIn = S.CSwitchThreadInOutCpu[i];
		if(!AllThreads[nThreadIn])
		{
		    AllThreads[nThreadIn] = '' + nThreadIn;
		    var FoundThread = false;
		    for(var i = 0; i < S.ThreadIds.length; ++i)
		    {
		        if(S.ThreadIds[i] == nThreadIn)
		        {
		            FoundThread = true;
		        }
		    }
		    if(!FoundThread)
		    {
		        S.CSwitchOnlyThreads.push(nThreadIn);
		    }
		}
	}
	for(var i = 0; i < S.CSwitchOnlyThreads.length; ++i)
	{
		PreprocessContextSwitchCacheItem(S.CSwitchOnlyThreads[i]);
	}
	for(var i = 0; i < S.ThreadIds.length; ++i)
	{
		PreprocessContextSwitchCacheItem(S.ThreadIds[i]);	
	}
	function HandleMissingThread(a)
	{
		if(!S.CSwitchThreads[a])
		{
			S.CSwitchThreads[a] = {'tid':a, 'pid':-1, 't':'?', 'p':'?'}
		}
	}
	function CompareThreadInfo(a, b)
	{
		if(a.pid != b.pid)
			return a.pid - b.pid;
		else
			return a.tid - b.tid;
	}
	S.CSwitchOnlyThreads.sort( function(a, b){ 
		HandleMissingThread(a);
		HandleMissingThread(b);
		return CompareThreadInfo(S.CSwitchThreads[a], S.CSwitchThreads[b]); 
	} );

	ProfileLeave();
}

function DrawContextSwitchBars(context, ThreadId, fScaleX, fOffsetY, fDetailedOffset, nHoverColor, MinWidth, bDrawEnabled, bSecond)
{
	ProfileEnter("DrawContextSwitchBars");
	var CSObject = S.CSwitchCache[ThreadId];
	if(CSObject && CSObject.Size > 0)
	{
		var Size = CSObject.Size;		
		var In = CSObject.In;
		var Out = CSObject.Out;
		var Cpu = CSObject.Cpu;
		var nNumColors = CSwitchColors.length;
		for(var i = 0; i < Size; ++i)
		{
			var TimeIn = In[i];
			var TimeOut = Out[i];
			var ActiveCpu = Cpu[i];

			var X = (TimeIn - fDetailedOffset) * fScaleX;
			if(X > nWidth)
			{
				break;
			}
			var W = (TimeOut - TimeIn) * fScaleX;
			if(W > MinWidth && X+W > 0)
			{
				if(nHoverCSCpu == ActiveCpu || bDrawEnabled)
				{
					if(nHoverCSCpu == ActiveCpu)
					{
						context.fillStyle = nHoverColor;
					}
					else
					{
						context.fillStyle = CSwitchColors[ActiveCpu % nNumColors];
					}
					context.fillRect(X, fOffsetY, W, CSwitchHeight);
				}
				if(DetailedViewMouseX >= X && DetailedViewMouseX <= X+W && DetailedViewMouseY < fOffsetY+CSwitchHeight && DetailedViewMouseY >= fOffsetY)
				{
					nHoverCSCpuNext = ActiveCpu;
					RangeCpuNext.Off = bSecond ? fDetailedOffsetSecond : 0;
					RangeCpuNext.Begin = TimeIn;
					RangeCpuNext.End = TimeOut;
					RangeCpuNext.Thread = ThreadId;
					RangeGpuNext.Begin = RangeGpuNext.End = -1;
				}
			}
		}
	}
	else 
	{
		//debug: draw context switch without cswitch
		// for(var i = 0; i < 8; ++i)
		// {
		// 	context.fillStyle = CSwitchColors[i];
		// 	context.fillRect((nWidth * i / 8) + 10, fOffsetY, nWidth / 8 - 20, CSwitchHeight);
		// }
	}
	ProfileLeave();
}

function DrawDetailedButtons(context, X, Y, Names, Callbacks, Align)
{
	if(!Align)
	{
		Align = "left";
	}
	var Dir = 1;
	var Offset = 0;
	var OffsetText = 0;
	if(Align == "right")
	{
		Dir = -1;
	}
	else if(Align == "center")
	{
		Offset = -0.5;
		Align = "left";
	}
	let W = Array(Names.length);
	var w = 0;
	for(let i = 0; i < Names.length; ++i)
	{
		W[i] = context.measureText(Names[i]).width;
		w += W[i];
	}
	X += w * Offset;

	// context.textBaseline = 'top';
	context.textAlign = Align;
	context.fillStyle = 'white';
	var mx = DetailedViewMouseX;
	var my = DetailedViewMouseY;
	for(let i = 0; i < Names.length; ++i)
	{
		let w = W[i];
		let x0_ = X + w * Dir;
		let x0 = Math.min(X, x0_);
		let x1 = Math.max(X, x0_);
		let y0 = Y;
		let y1 = Y + FontHeight;
		context.fillStyle = nBackColors[0];
		context.fillRect(x0, y0, x1 - x0, y1 - y0);

		if(mx >= x0 && mx <= x1 && my >= y0 && my <= y1)
		{
			DetailedMouseOverButton = 1;
			context.fillStyle = 'cyan';
			DetailedAddMouseEvent(99, function(){ Callbacks[i](); } );
		}
		else
		{
			context.fillStyle = 'white';
		}

		context.fillText(Names[i], X, Y + FontHeight - 2);
		X += Dir * (w+FontWidth);
	}

	context.fillStyle = 'white';
	context.textAlign = 'left';
}

function DrawDetailedView(S, S0, context, MinWidth, bDrawEnabled)
{
	if(!S || !S.Frames)
	{
		return;
	}
	let bSecond = S != S0;
	if(bDrawEnabled && !bSecond)
	{
		DrawDetailedBackground(context);
	}

	let colors = [ '#ff0000', '#ff00ff', '#ffff00'];
	let fScaleX = nWidth / fDetailedRange; 
	let fOffsetY = -nOffsetY + BoxHeight;


	let nHoverTokenStack = -1;
	if(S == S0)
	{
		nHoverCounter += nHoverCounterDelta;
		if(nHoverCounter >= 255) 
		{
			nHoverCounter = 255;
			nHoverCounterDelta = -nHoverCounterDelta;
		}
		if(nHoverCounter < 128) 
		{
			nHoverCounter = 128;
			nHoverCounterDelta = -nHoverCounterDelta;
		}
	}
	let nHoverHigh = nHoverCounter.toString(16);
	let nHoverLow = (127+255-nHoverCounter).toString(16);
	let nHoverColor = '#' + nHoverHigh + nHoverHigh + nHoverHigh;
	let nHoverColorIndex;

	context.fillStyle = 'black';
	context.font = Font;
	// context.textBaseline = 'alphabetic';

	let nNumLogs = S.Frames[0].ts.length;
	if(S0)
	{
		let l0 = S0.Frames[0].ts.length;
		let l1 = S.Frames[0].ts.length
		if(l1 != l0)
			debugger;
	}
	let Off = fDetailedOffset;
	let XOffset = 0;
	if(bSecond)
	{
		XOffset = fDetailedOffsetSecond;
		Off += XOffset;
	}
	let fTimeEnd = Off + fDetailedRange;

	let FirstFrame = 0;
	for(let i = 0; i < S.Frames.length ; i++)
	{
		if(S.Frames[i].frameend < Off)
		{
			FirstFrame = i;
		}
	}

	let BoxHeightScaled = BoxHeight;
	let Scale = 1;
	let fExtra = 0;
	let Padding = 15;
	let SecondReverseOffset = 0;
	if(DrawDetailedFlameMode == 1)
	{
		BoxHeightScaled = Math.ceil(BoxHeight*0.5);
		fExtra = 0.5;
	}
	else if(DrawDetailedFlameMode == 2)
	{
		BoxHeightScaled = 0;
		fExtra = 1;
	}
	if(DrawDetailedCompareReverse && bSecond)
	{
		Scale = -1;
		BoxHeightScaled = -BoxHeightScaled;
		SecondReverseOffset = 5-BoxHeight;
	}
	let BoxHeightScaledPos = Math.abs(BoxHeightScaled);
	let Levels = [];
	let BatchesTxtColor = ['#ffffff', '#333333'];

	let AddBatch = function(StackPos, index, X, Y, W, Name, NameLen, Duration)
	{
		let L = Levels[StackPos];
		if(!L)
		{
			for(let j = 0; j < StackPos+1; ++j)
			{
				if(!Levels[j])
				{
					Levels[j] = {};
					L = Levels[j];
					L.Batches = new Array(g_Colors.length+1);
					L.BatchesTxt = new Array();
					L.BatchesTxtPos = new Array();
					for(let i = 0; i < 2; ++i)
					{
						L.BatchesTxt[i] = Array();
						L.BatchesTxtPos[i] = Array();
					}
					for(let i = 0; i < L.Batches.length; ++i)
					{
						L.Batches[i] = Array();
					}
				}
			}
		}
		let B = L.Batches[index];
		let txtidx = g_ColorsTextIndex[index];
		if(txtidx < 0 || txtidx > 1)
			debugger;
		let BTxt = L.BatchesTxt[txtidx];
		let BTxtPos = L.BatchesTxtPos[txtidx];
		B.push(X);
		B.push(Y);
		B.push(W);
		DebugDrawQuadCount++;

		let XText = X < 0 ? 0 : X;
		let WText = W - (XText-X);
		if(XText + WText > nWidth)
		{
			WText = nWidth - XText;
		}
		let BarTextLen = Math.floor((WText-2)/FontWidth);
		let TimeText = TimeToMsString(Duration);
		let TimeTextLen = TimeText.length;

		if(BarTextLen >= 2)
		{
			if(BarTextLen < NameLen)
				Name = Name.substr(0, BarTextLen);
			let YPos = Y+BoxHeight-FontAscent;
			BTxt.push(Name);
			BTxtPos.push(XText+2);

			BTxtPos.push(YPos);
			DebugDrawTextCount++;
			if(BarTextLen - NameLen > TimeTextLen)
			{
				BTxt.push(TimeText);
				BTxtPos.push(XText+WText-2 - TimeTextLen * FontWidth);
				BTxtPos.push(YPos);
				DebugDrawTextCount++;
			}
		}
	};
	{
		context.fillStyle = 'white';
		fOffsetY += BoxHeight;
		context.fillText("Timeline", 0, fOffsetY);
		fOffsetY += BoxHeight;


		let Times = Timeline.Times;
		let Colors = Timeline.Colors;
		let Ends = Timeline.Ends;
		let Positions = Timeline.Positions;
		let Names = Timeline.Names;
		let off = 0.7;
		let off2 = 2*off;
		let MaxPosition = 1;

		for(let i = 0; i < Timeline.Positions.length; ++i)
		{
			let TimeStart = Times[i];
			let TimeEnd = Ends[i];
			let Position = Positions[i];
			if(Position >= 0)
			{
				MaxPosition = Math.max(Position, MaxPosition);
				let Color = Colors[i];
				let Name = Names[i];
				let P = Positions[i];
				let X = (TimeStart - Off) * fScaleX;
				let Y = fOffsetY + P * (BoxHeight+2);
				let W = (TimeEnd-TimeStart)*fScaleX;
				if(!bSecond)
				{
					if(DetailedViewMouseX >= X && DetailedViewMouseX <= X+W && DetailedViewMouseY < Y+BoxHeight && DetailedViewMouseY >= Y)
					{
						RangeCpuNext.Off = 0;
						RangeCpuNext.Begin = TimeStart;
						RangeCpuNext.End = TimeEnd;
						RangeCpuNext.Thread = -1;
						nHoverColorIndex = Color;
						Color = cidhovercolor;
					}
					AddBatch(0, Color, X, Y, W, Name, Name.length, TimeEnd-TimeStart);
				}
			}
		}
		fOffsetY += (1+MaxPosition) * (BoxHeight+3);
	}
	let nMinTimeMs = MinWidth / fScaleX;
	let AutoHideCount = 0;
	let VisibleThreadCount = 0;
	let DrawGuides = 1;
	{
		if(!ThreadYBegin)
		{
			ThreadYBegin = Array(S0.ThreadNames.length);
			ThreadYEnd = Array(S0.ThreadNames.length);
		}

		for(let i = 0; i < nNumLogs; i++)
		{
			let nLog = ThreadOrderT[i];
			let ThreadName = S0.ThreadNames[nLog];
			let Active = ThreadsActive[ThreadName];
			let ThreadColors = S0.ThreadColors[nLog];
			ThreadYBegin[nLog] = fOffsetY;
			if(0 != S.ThreadLogAutoHidden[nLog])
			{
				AutoHideCount++;
				continue;
			}
			if(HideMode == HideModeCollapsed && !Active)
			{
				continue;
			}
			VisibleThreadCount++;
			let SMul = 2;
			let LogHeight = (S0.MaxStack[nLog]) * BoxHeightScaledPos + BoxHeight;
			let LogHeight2 = (S0.MaxStack2[nLog]) * BoxHeightScaledPos + BoxHeight;
			if(nContextSwitchEnabled)
			{
				LogHeight += (CSwitchHeight+4);
				LogHeight2 += (CSwitchHeight+4);
			}
			if(!S0.SecondActive)
			{
				LogHeight2 = 0;
			}
			let OY = fOffsetY;
			if(!bSecond)
			{
				OY += LogHeight2+1;
			}
			else
			{
				if(DrawDetailedCompareReverse)
				{
					OY += LogHeight2 - 5;
				}
			}
			
			let fOffsetYDelta = Active ? (LogHeight + LogHeight2 + Padding) : BoxHeight;

			ThreadYBegin[nLog] = fOffsetY;
			ThreadYEnd[nLog] = fOffsetY + fOffsetYDelta;

			let ThreadHover = false;
			if(bDrawEnabled)
			{
				if(Active || !bSecond)
				{
					let Color = bSecond ? ThreadColors.coloroff : ThreadColors.color;
					let ColorLine = !bSecond ? ThreadColors.colordark : ThreadColors.color;
					let Gradient = bSecond ? ThreadColors.gradientoff : ThreadColors.gradient;
					let BaseY = (bSecond||!Active) ? (fOffsetY+1) : (fOffsetY+1+LogHeight2);
					let HeightY = !Active ? BoxHeight : (bSecond ? LogHeight2 : (LogHeight + Padding));
					let TransparentLine = S2.Frames != null && !bSecond;
					if(TransparentLine)
					{
						context.globalAlpha = 0.4;
					}
					context.strokeStyle = ColorLine;
					context.beginPath();
					context.moveTo(0, BaseY);
					context.lineTo(nWidth, BaseY);
					context.stroke();
					if(TransparentLine)
					{
						context.globalAlpha = 1.0;
					}

					if(!Gradient)
					{
						Gradient = context.createLinearGradient(0,0,200,0);
						Gradient.addColorStop(0, Color);
						Gradient.addColorStop(1,'transparent');
						if(bSecond)
							ThreadColors.gradientoff = Gradient;
						else
							ThreadColors.gradient = Gradient;
					}
					context.fillStyle = Gradient;
					context.fillRect(0,BaseY,200,HeightY);
				}
			}
			let D = fOffsetY + BoxHeight - DetailedViewMouseY;
			
			if(bDrawEnabled && G_DEBUG && 0)
			{
				debugger;
			}
			if(!bSecond)
			{
				if(D > 0 && D < BoxHeight)
				{
					if(!bDrawEnabled)
					{
						context.globalAlpha = 0.3;
						context.fillStyle = 'grey';
						context.fillRect(0, fOffsetY+2, nWidth, Active ? LogHeight2 + LogHeight + Padding : BoxHeight);
						context.globalAlpha = 1.0;
					}
					ThreadHover = 1;
				}
			}

			if(Active)
			{
				let LodIndex = 0;
				let MinDelta = 0;
				let NextLod = 1;
				while(NextLod < S.LodData.length && S.LodData[NextLod].MinDelta[nLog] < nMinTimeMs)
				{
					LodIndex = NextLod;
					NextLod = NextLod + 1;
					MinDelta = S.LodData[LodIndex].MinDelta[nLog];
				}
				if(LodIndex == S.LodData.length)
				{
					LodIndex = S.LodData.length-1;
				}
				if(DisableLod || FilterSearchPassIndex != -1)
				{
					LodIndex = 0;
				}

				OY += 4;

				if(nContextSwitchEnabled)
				{
					DrawContextSwitchBars(context, S.ThreadIds[nLog], fScaleX, OY, Off, nHoverColor, MinWidth, bDrawEnabled, bSecond);
					OY += Scale * (CSwitchHeight+2);
				}
				OY += SecondReverseOffset;
				let MaxDepth = 1;
				let StackPos = 0;
				let Stack = Array(20);
				let Lod = S.LodData[LodIndex];

				let TypeArray = Lod.TypeArray[nLog];
				let IndexArray = Lod.IndexArray[nLog];
				let TimeArray = Lod.TimeArray[nLog];

				let LocalFirstFrame = S.Frames[FirstFrame].FirstFrameIndex[nLog];
				let IndexStart = S.ISGPU[nLog] ? 0 : Lod.LogStart[LocalFirstFrame][nLog];
				let IndexEnd = TimeArray.length;
				IndexEnd = TimeArray.length;
				let HasSetHover = 0;



				for(let j = IndexStart; j < IndexEnd; ++j)
				{
					let type = TypeArray[j];
					let index = IndexArray[j];
					let time = TimeArray[j];
					if(type == 1)
					{
						//push
						Stack[StackPos] = j;
						StackPos++;
						if(StackPos > MaxDepth)
						{
							MaxDepth = StackPos;
						}
					}
					else if(type == 0)
					{
						if(StackPos>0)
						{
							StackPos--;

							let StartIndex = Stack[StackPos];
							let timestart = TimeArray[StartIndex];
							let timeend = time;
							let X = (timestart - Off) * fScaleX;
							let Y = OY + StackPos * BoxHeightScaled;

							let W = (timeend-timestart)*fScaleX;

							if(X < nWidth && X+W > 0)
							{
								if(index == FilterSearchPassIndex)
								{
									let Range = RangeInit();
									Range.Begin = timestart;
									Range.End = timeend;
									Range.Thread = nLog;
									Range.YBegin = Y - ThreadYBegin[nLog];
									Range.Second = bSecond;

									if(FilterSearchArray.length < 500)
									{
										FilterSearchArray.push(Range);
										if(FilterSearchArray.length == 500)
										{
											ShowFlashMessage('Capping Search Result to 500', 30);
										}
									}
								}
								if(W > MinWidth)
								{
									if(bDrawEnabled || index == nHoverToken)
									{
										let cid = GroupColors == 2 ? ThreadColors.colordark_cid : S0.TimerInfo[index].cid;
										if(index == nHoverToken)
										{
											nHoverColorIndex = cid;
											cid = cidhovercolor;
										}
										AddBatch(StackPos, cid, X, Y, W, S0.TimerInfo[index].name, S0.TimerInfo[index].len, timeend-timestart);
									}


									if(DetailedViewMouseX >= X && DetailedViewMouseX <= X+W && DetailedViewMouseY < Y+BoxHeight && DetailedViewMouseY >= Y && StackPos > nHoverTokenStack)
									{
										RangeCpuNext.Off = bSecond ? fDetailedOffsetSecond : 0;
										RangeCpuNext.Begin = timestart;
										RangeCpuNext.End = timeend;
										RangeCpuNext.Thread = nLog;
										RangeCpuNext.Off = XOffset;
										if(TypeArray[StartIndex+1] == 3 && TypeArray[j+1] == 3)
										{
											RangeGpuNext.Begin = RangeCpuNext.Begin;
											RangeGpuNext.End = RangeCpuNext.End;
											RangeGpuNext.Thread = nLog;
											RangeGpuNext.Off = XOffset;
											//cpu tick is stored following
											RangeCpuNext.Begin = TimeArray[StartIndex+1];
											RangeCpuNext.End = TimeArray[j+1];
											RangeCpuNext.Thread = IndexArray[StartIndex+1];
											RangeCpuNext.Off = XOffset;
										}
										else
										{
											RangeGpuNext.Begin = -1;
											RangeGpuNext.End = -1
										}

										nHoverTokenNext = index;
										nHoverTokenStack = StackPos;
										nHoverTokenIndexNext = j;
										nHoverTokenLogIndexNext = nLog;
										HoverTokenNextOwner = S;
										HasSetHover = 1;
									}
								}
							}
							if(StackPos == 0 && time > fTimeEnd)
								break;											
						}
					}
				}
				if(HasSetHover)
				{
					for(let i = 0; i < S.Frames.length-1; ++i)
					{
						let IndexStart = Lod.LogStart[i][nLog];
						if(nHoverTokenIndexNext >= IndexStart)
						{
							nHoverFrame = i;
						}
					}
				}
			}
			let w = context.measureText(ThreadName).width;
			context.fillStyle = nBackColors[0];
			context.fillStyle = 'white';
			context.fillText(ThreadName, 0, fOffsetY+FontHeight-1);
			if(ThreadHover)
			{
				DrawDetailedButtons(context, nWidth, fOffsetY+1, ["\u2191\u2191\u2191", "\u2193\u2193\u2193", "\u2191", "\u2193"], 
					[function(){ThreadOrderMoveUp(ThreadName,1);}, function(){ThreadOrderMoveDown(ThreadName,1);},
					function(){ThreadOrderMoveUp(ThreadName);}, function(){ThreadOrderMoveDown(ThreadName);}],
					 "right");
							// DetailedAddMouseEvent(1, function(){ Callbacks[i](); } );
				DetailedAddMouseEvent(1, function()
				{
					ToggleThread(ThreadName);
				});
			}
			// context.textBaseline = 'alphabetic';
			fOffsetY += fOffsetYDelta;
			// ThreadY[nLog+1] = fOffsetY;
		}
		if(AutoHideCount)
		{
			fOffsetY += FontHeight;
			let Text = AutoHideCount + " Threads Auto Hidden. Toggle in Thread Menu to show";
			let w = context.measureText(Text).width;
			context.fillStyle = "orange";
			context.fillText(Text, 0, fOffsetY+FontHeight-1);
		}
		if(VisibleThreadCount == 0 && HideMode == HideModeCollapsed)
		{
			fOffsetY += FontHeight;
			let Text = "All Threads Hidden. Toggle in Thread Menu to show";
			let w = context.measureText(Text).width;
			context.fillStyle = "red";
			context.fillText(Text, 0, fOffsetY+FontHeight-1);
		}

		if(nContextSwitchEnabled && !bSecond) //non instrumented threads.
		{
			let CurrentPid = -112;
			let ContextSwitchThreads = S.CSwitchOnlyThreads;
			function DrawHeader(str, X, Y)
			{
				let width = str.length * FontWidth;
				context.globalAlpha = 0.5;
				context.fillStyle = 'grey';
				context.fillRect(X, Y-FontHeight + 2, width, FontHeight);
				context.globalAlpha = 1.0;
				context.fillStyle = 'white';
				context.fillText(str, X, Y);


			}
			for(let i = 0; i < ContextSwitchThreads.length; ++i)
			{
				let ThreadId = ContextSwitchThreads[i];
				let ThreadName = '' + ThreadId;
				let TI = S.CSwitchThreads[ThreadId];

				if(TI)
				{
					if(CurrentPid != TI.pid)
					{
						fOffsetY += BoxHeight + 1;
						CurrentPid = TI.pid;
						let str = TI.pid.toString(16) +':' +TI.p;
						DrawHeader(str, 0, fOffsetY+5);
						fOffsetY += BoxHeight + 1;
					}
				}

				DrawContextSwitchBars(context, ThreadId, fScaleX, fOffsetY, Off, nHoverColor, MinWidth, bDrawEnabled);

				if(TI)
				{
					DrawHeader(TI.tid.toString(16) +':' +TI.t, 10, fOffsetY+5);
 				}
				fOffsetY += BoxHeight + 1;
			}
		}
		{
			let h = nHeight;

		}
		let TOP_LIMIT = 100;
		if(fOffsetY < TOP_LIMIT)
		{
			let Diff = fOffsetY - TOP_LIMIT;
			nOffsetY += Math.floor(Diff);
			nOffsetY = Math.max(0, nOffsetY);
		}
		let FillRects = function(a, color, colordark)
		{
			context.fillStyle = color;
			for(let j = 0; j < a.length; j += 3)
			{						
				let X = a[j];
				let Y = a[j+1];
				let W = a[j+2];
				if(W >= 1)
				{
					context.fillRect(X, Y, W, BoxHeight-1);
				}
			}
			// context.fillStyle = color;
			// for(var j = 0; j < a.length; j += 3)
			// {						
			// 	var X = a[j];
			// 	var Y = a[j+1];
			// 	var W = a[j+2];
			// 	if(W > 0)
			// 	{
			// 		context.fillRect(X+off, Y+off, W-off2, BoxHeight-1-off2);
			// 	}
			// }
		}

		for(let l = 0; l < Levels.length; ++l)
		{
			let Batches = Levels[l].Batches;
			for(let i = 0; i < Batches.length; ++i)
			{
				let a = Batches[i];
				if(a.length)
				{
					if(!DisableMerge)
					{
						for(let j = 0; j < a.length; j += 3)
						{						
							let X = a[j];
							let Y = a[j+1];
							let BaseWidth = j + 2;
							let W = a[BaseWidth];
							while(j+1 < a.length && W < 1)
							{
								let jnext = j+3;
								let XNext = a[jnext];
								let YNext = a[jnext+1];
								let WNext = a[jnext+2];
								let Delta = XNext - (X+W);
								let YDelta = Math.abs(Y - YNext);							
								if(Delta < 0.3 && YDelta < 0.5 && WNext < 1)
								{
									W = (XNext+WNext) - X;
									a[BaseWidth] = W;
									a[jnext+2] = 0;
									j += 3;
								}
								else
								{
									break;
								}

							}
						}
					}
					let Color = i == 0 ? nHoverColor : g_Colors[i];
					let ColorDark = i == 0 ? nHoverColor : g_ColorsDark[i];
					if(i == 0)
					{
						if(nHoverColorIndex)
						{
							let mult = nHoverCounter / 255.0;
							let m2 =mult -0.5;
							let m4 = m2 * 0.5;
							let f = function()
							{
								let H = g_ColorH[nHoverColorIndex];
								let S = g_ColorS[nHoverColorIndex];
								let L = g_ColorL[nHoverColorIndex];
								let offset = 0;
								if(S> 0.5)
									offset = 0.5 - S;
								let offsetL = 0;
								if(L > 0.75)
									offsetL = 0.75 - L;
								L = L + m4 + offsetL;
								L = L > 1 ? 1 : (L < 0 ? 0 : L);
								S = S + m2 + offset;
								S = S > 1 ? 1 : (S < 0 ? 0 : S);
								return ConvertHslToColor(H, S, L);
							};
							let ColorCV = f();
							ColorDark = Color = ColorCV; ;
						}
					}
					FillRects(a, Color, ColorDark);

				}
			}
			let BatchesTxt = Levels[l].BatchesTxt;
			let BatchesTxtPos = Levels[l].BatchesTxtPos;	
			for(let i = 0; i < BatchesTxt.length; ++i)
			{
				context.fillStyle = BatchesTxtColor[i];
				let TxtArray = BatchesTxt[i];
				let PosArray = BatchesTxtPos[i];
				for(let j = 0; j < TxtArray.length; ++j)
				{
					let k = j * 2;
					context.fillText(TxtArray[j], PosArray[k],PosArray[k+1]);
				}
			}

		}
	}
}
function DrawTextBox(context, text, x, y, align)
{
	let textsize = context.measureText(text).width;
	let offsetx = 0;
	let offsety = -FontHeight;
	if(align == 'center')
	{
		offsetx = -textsize / 2.0;
	}
	else if(align == 'right')
	{
		offsetx = -textsize;
	}
	context.fillStyle = nBackColors[0];
	context.fillRect(x + offsetx, y + offsety, textsize+2, FontHeight + 2);
	context.fillStyle = 'white';
	context.fillText(text, x, y);

}

function DrawFilterSearchRanges(context, Ranges, ColorBack, ColorFront, ThreadY)
{
	if(0 == Ranges.length)
		return;
	//todo: arrow key support 
	var MarginTop = 1.0 * FontHeight;
	var MarginBottom = nHeight - 1.0 * FontHeight;


	var Time = new Date();
	var Delta = Time - FilterSearchStartTime;
	var BlinkTime = 2.5 * 1000;
	var Blinks = 5;
	var HoverFloat = 0.2;
	var Blinking = 0;
	if(Delta < BlinkTime)
	{
		Blinking = 1;
		var b0 = Blinks* Math.PI * Delta;
		var b1 = b0 / BlinkTime;
		var Mag = Math.abs(Math.sin(b1));
		HoverFloat = Mag * 0.5 + 0.2;
	}
	var Lines = new Array();
	var Rects = new Array();


	for(var i = 0; i < Ranges.length; ++i)
	{
		var Range = Ranges[i];
		let Off = Range.Second ? fDetailedOffsetSecond : 0
		var fBegin = Range.Begin - Off;
		var fEnd = Range.End - Off;
		var OffsetTop = Range.YBegin + ThreadY[Range.Thread];
		var OffsetBottom = OffsetTop + BoxHeight;

		if(fBegin < fEnd)
		{
			{
				OffsetTop = Math.max(OffsetTop, 0);
				OffsetBottom = Math.max(OffsetBottom, MarginTop);
				OffsetTop = Math.min(OffsetTop, MarginBottom);
				OffsetBottom = Math.min(OffsetBottom, nHeight);
				var fScaleX = nWidth / fDetailedRange; 
				var X = (fBegin - fDetailedOffset) * fScaleX;
				var Y = OffsetTop;
				var W = (fEnd - fBegin) * fScaleX;
				if(W > 1)
				{
					Rects.push(X-3);
					Rects.push(OffsetTop-3);
					Rects.push(W+5);
					Rects.push(5 + OffsetBottom - OffsetTop);
				}
				else
				{
					X += W * 0.5;
					Lines.push(X);
					context.beginPath();
					context.moveTo(X, 0);
					context.lineTo(X, nHeight);
					context.stroke();
				}
			}
		}
	}

	context.fillStyle = ColorBack;
	context.strokeStyle = ColorBack;

	context.globalAlpha = HoverFloat;

	for(var i = 0; i < Rects.length;)
	{
		var X = Rects[i++];
		var Y = Rects[i++];
		var W = Rects[i++];
		var H = Rects[i++];
		context.fillRect(X, Y, W, H);
	}
	context.globalAlpha = 1;
	for(var i = 0; i < Rects.length;)
	{
		var X = Rects[i++];
		var Y = Rects[i++];
		var W = Rects[i++];
		var H = Rects[i++];
		context.strokeRect(X, Y, W, H);
	}

	for(var i = 0; i < Lines.length; ++i)
	{
		context.beginPath();
		context.moveTo(Lines[i], 0);
		context.lineTo(Lines[i], nHeight);
		context.stroke();
	}
	if(Blinking>0)
	{
		Invalidate = 1;
	}

}


function DrawRange(context, Range, ColorBack, ColorFront, Name, Offset)
{
	if(!Offset)
		Offset = 0;
	var fBegin = Range.Begin;
	var fEnd = Range.End;
	var OffsetTop = Range.YBegin;
	var OffsetBottom = Range.YEnd;
	var Off = Range.Off + fDetailedOffset;
	if(fBegin < fEnd)
	{
		var MarginTop = (1.0+Offset) * (FontHeight+1);
		var MarginBottom = nHeight - (Offset+1.5) * (FontHeight+1);
		if(OffsetTop < MarginTop)
		{
			Offset += 1;
			OffsetTop = MarginTop;
		}
		if(OffsetBottom > MarginBottom)
		{
			OffsetBottom = MarginBottom;
		}
		var fRulerOffset = FontHeight * 0.5;
		var fScaleX = nWidth / fDetailedRange; 
		var X = (fBegin - Off) * fScaleX;
		var YSpace = (FontHeight+2);
		var Y = OffsetTop;
		var YBottom = OffsetBottom;
		var W = (fEnd - fBegin) * fScaleX;
		context.globalAlpha = 0.1;
		context.fillStyle = ColorBack;
		context.fillRect(X, OffsetTop + fRulerOffset, W, OffsetBottom - OffsetTop);
		context.globalAlpha = 1;
		context.strokeStyle = ColorFront;
		context.beginPath();
		context.moveTo(X, 0);
		context.lineTo(X, nHeight);
		context.moveTo(X+W, 0);
		context.lineTo(X+W, nHeight);
		context.stroke();
		var Duration = (fEnd - fBegin).toFixed(2) + "ms";
		var Center = ((fBegin + fEnd) / 2.0) - Off;
		var DurationWidth = context.measureText(Duration+ "   ").width;

		context.fillStyle = 'white';
		context.textAlign = 'right';
		var TextPosY = Y + YSpace;
		DrawTextBox(context, (fBegin-Range.Off).toFixed(2), X-3, TextPosY, 'right');
		var YS = [Y, YBottom];
		for(var i = 0; i < YS.length; ++i)
		{
			var Y = YS[i];
			var Y0 = Y + fRulerOffset;
			var W0 = W - DurationWidth + FontWidth*1.5;
			if(W0 > 6)
			{
				context.textAlign = 'center';
				DrawTextBox(context, Duration,Center * fScaleX, Y + YSpace, 'center');
				W0 = W0 / 2.0;
				var X0 = X + W0;
				var X1 = X + W - W0;
				context.strokeStyle = ColorFront;
				context.beginPath();
				context.moveTo(X, Y0);
				context.lineTo(X0, Y0);
				context.moveTo(X0, Y0-2);
				context.lineTo(X0, Y0+2);
				context.moveTo(X1, Y0-2);
				context.lineTo(X1, Y0+2);
				context.moveTo(X1, Y0);
				context.lineTo(X + W, Y0);
				context.stroke();
			}
			else
			{
				if(i == 1)
				{
					context.textAlign = 'right';
					DrawTextBox(context, Duration, X - 3, Y0, 'right');
					context.textAlign = 'left';
					DrawTextBox(context, Duration, X + W + 2, Y0, 'left');
				}
				context.strokeStyle = ColorFront;
				context.beginPath();
				context.moveTo(X, Y0);
				context.lineTo(X+W, Y0);
				context.stroke();
			}
		}
		context.textAlign = 'left';
		DrawTextBox(context, (fEnd-Range.Off).toFixed(2), X + W + 2, TextPosY, 'left');
		DrawTextBox(context, Name, X + W + 2, OffsetTop + YSpace + FontHeight, 'left');
	}
	return Offset;
}
function DetailedAddMouseEvent(P, E)
{
	if(!DetailedMouseEvent)
	{
		DetailedMouseEvent = {"P":P, "E":E};
	}
	else if(DetailedMouseEvent.P < P)
	{
		DetailedMouseEvent.P = P;
		DetailedMouseEvent.E = E;
	}

}

function DrawDetailed(Animation)
{
	DetailedMouseEvent = null;

	if(!Initialized)
	{
		return;
	}
	ProfileEnter("DrawDetailed");
	DetailedMouseOverButton = false;

	DebugDrawQuadCount = 0;
	DebugDrawTextCount = 0;
	nHoverCSCpuNext = -1;

	RangeCpuNext = RangeInit();
	RangeGpuNext = RangeInit();

	var start = new Date();
	nDrawCount++;

	var context = CanvasDetailedView.getContext('2d');
	var offscreen = CanvasDetailedOffscreen.getContext('2d');
	var fScaleX = nWidth / fDetailedRange; 
	var fOffsetY = -nOffsetY + BoxHeight;
	if(DetailedRedrawState.fOffsetY == fOffsetY && DetailedRedrawState.fDetailedOffset == fDetailedOffset && DetailedRedrawState.fDetailedRange == fDetailedRange && !KeyCtrlDown && !KeyShiftDown && !KeyAltDown && !MouseDragButton && !RedrawRequested)
	{
		Invalidate++;
	}
	else
	{
		Invalidate = 0;
		DetailedRedrawState.fOffsetY = fOffsetY;
		DetailedRedrawState.fDetailedOffset = fDetailedOffset;
		DetailedRedrawState.fDetailedRange = fDetailedRange;
	}
	if(nHoverTokenDrawn != nHoverToken)
	{
		Invalidate = 1;
	}
	nHoverTokenDrawn = nHoverToken;
	nHoverTokenNext = -1;
	HoverTokenNextOwner = null;
	nHoverTokenLogIndexNext = -1;
	nHoverTokenIndexNext = -1;



	if(Invalidate == 0) //when panning, only draw bars that are a certain width to keep decent framerate
	{
		context.clearRect(0, 0, CanvasDetailedView.width, CanvasDetailedView.height);
		DrawDetailedView(S, S, context, nMinWidthPan, true);
		DrawDetailedView(S2, S, context, nMinWidthPan, true, 1);
		ProfileRedraw0++;
	}
	else if(Invalidate == 1) //draw full and store
	{
		offscreen.clearRect(0, 0, CanvasDetailedView.width, CanvasDetailedView.height);
		DrawDetailedView(S, S, offscreen, nMinWidth, true);
		DrawDetailedView(S2, S, offscreen, nMinWidth, true, 1);
		OffscreenData = offscreen.getImageData(0, 0, CanvasDetailedOffscreen.width, CanvasDetailedOffscreen.height);
		ProfileRedraw1++;
	}
	else//reuse stored result untill next time viewport is changed.
	{
		context.clearRect(0, 0, CanvasDetailedView.width, CanvasDetailedView.height);
		context.putImageData(OffscreenData, 0, 0);
		DrawDetailedView(S, S, context, nMinWidth, false);
		DrawDetailedView(S2, S, context, nMinWidth, false, 1);
		ProfileRedraw2++;
	}

	if(KeyShiftDown || KeyCtrlDown || MouseDragSelectRange() || ZoomActive || FilterSearchActive)
	{
		nHoverToken = -1;
		nHoverTokenIndex = -1;
		nHoverTokenLogIndex = -1;
		HoverTokenOwner = null;
		RangeCpu = RangeInit();
		RangeGpu = RangeInit();
	}
	else
	{
		nHoverToken = nHoverTokenNext;
		HoverTokenOwner = HoverTokenNextOwner;
		nHoverTokenIndex = nHoverTokenIndexNext;
		nHoverTokenLogIndex = nHoverTokenLogIndexNext;
		if(RangeValid(RangeCpuHistory))
		{
			RangeCopy(RangeCpu, RangeCpuHistory);
			RangeCopy(RangeGpu, RangeGpuHistory);
		}
		else
		{
			RangeCopy(RangeCpu, RangeCpuNext);
			RangeCopy(RangeGpu, RangeGpuNext);
		}
	}

	DrawTextBox(context, TimeToMsString(fDetailedOffset), 0, FontHeight, 'left');
	context.textAlign = 'right';
	DrawTextBox(context, TimeToMsString(fDetailedOffset + fDetailedRange), nWidth, FontHeight, 'right');
	context.textAlign = 'left';

	if(!FilterSearchActive && !IgnoreInput && SubMenuActive == -1)
	{
		let Colors = GroupColors == 2 ? "Color[thread]" : (GroupColors ? "Color[group]" : "Color[timer]");
		DrawDetailedButtons(context, nWidth * 0.5, 0, [Colors, "Search"],[function(){ToggleGroupColors(1);}, FilterInputShow], "center");
	}


	var YBegin = ThreadYBegin[fRangeThreadIdNext];
	var YEnd = ThreadYEnd[fRangeThreadIdNext];
	var YBeginGpu = YBegin;
	var YEndGpu = YEnd;
	function RangeSet(R)
	{
		if(R.Thread >= 0)
		{
			R.YBegin = ThreadYBegin[R.Thread];
			R.YEnd = ThreadYEnd[R.Thread];
		}
		else
		{
			R.YBegin = 0;
			R.YEnd = nHeight;
		}
	}
	RangeSet(RangeSelect);
	RangeSet(RangeCpu);
	RangeSet(RangeGpu);
	var Offset = 0;
	Offset = DrawRange(context, RangeSelect, '#59d0ff', '#00ddff', "Selection", Offset);
	Offset = DrawRange(context, RangeCpu, '#009900', '#00ff00', "Cpu", Offset);
	Offset = DrawRange(context, RangeGpu, '#996600', '#775500', "Gpu", Offset);

	DrawFilterSearchRanges(context, FilterSearchArray, '#ff9900', '#ff9900', ThreadYBegin);

	nHoverCSCpu = nHoverCSCpuNext;

	DrawFilterSearch();


	ProfileLeave();
}
function ZoomToHighlight(NoGpu)
{
	if(RangeValid(RangeGpu) && !NoGpu)
	{
		ZoomToRange(RangeGpu);
	}
	else if(RangeValid(RangeCpu))
	{
		ZoomToRange(RangeCpu);
	}
	RangeCpu = RangeInit();
	RangeGpu = RangeInit();
}

function MoveToNext(Direction) //1 forward, -1 backwards
{
	var fTimeBegin, fTimeEnd, nLog;
	var Index = nHoverToken;

	if(nHoverToken != -1 && nHoverTokenLogIndex != -1)
	{
		fTimeBegin = RangeCpu.Begin;
		fTimeEnd = RangeCpu.End;
		nLog = nHoverTokenLogIndex;
	}
	else if(RangeValid(RangeSelect))
	{
		fTimeBegin = RangeSelect.Begin;
		fTimeEnd = RangeSelect.End;
		nLog = RangeSelect.Thread;
		Index = RangeSelect.Index;
	}
	else
	{
		return;
	}
	if(nLog<0)
	{
		return;
	}
	var Forward = Direction && Direction < 0 ? 0 : 1;
	var bFound = false;
	var nStackPos = 0;
	var fResultTimeBegin, fResultTimeEnd;
	var TypeBegin = Forward ? 1 : 0;
	var TypeEnd = Forward ? 0 : 1;
	var SearchTimeBegin = Forward ? fTimeBegin : fTimeEnd;

	var istart = Forward ? 0 : S.Frames.length-1;
	var iend = Forward ? S.Frames.length : -1;
	var idelta = Forward ? 1 : -1;
	for(var i = istart; i != iend; i += idelta)
	{
		var fr = S.Frames[i];
		var ts = fr.ts[nLog];
		var ti = fr.ti[nLog];
		var tt = fr.tt[nLog];
		var jstart = Forward ? 0 : ts.length-1;
		var jend = Forward ? ts.length : -1;
		var jdelta = Forward ? 1 : -1;
		for(var j = jstart; j != jend; j += jdelta)
		{
			if(!bFound)
			{
				if(tt[j] == TypeBegin && Index == ti[j])
				{
					if(SearchTimeBegin == ts[j])
					{
						bFound = true;
					}
				}
			}
			else
			{
				if(Index == ti[j])
				{
					var type = tt[j];
					if(type == TypeBegin)
					{
						if(0 == nStackPos)
						{
							fResultTimeBegin = ts[j];
						}
						nStackPos++;
					}
					else if(type == TypeEnd && nStackPos)
					{
						nStackPos--;
						if(0 == nStackPos)
						{
							fResultTimeEnd = ts[j];
							if(0 == Forward)
							{
								var Tmp = fResultTimeBegin;
								fResultTimeBegin = fResultTimeEnd;
								fResultTimeEnd = Tmp;
							}
							RangeSelect.Begin = fResultTimeBegin;
							RangeSelect.End = fResultTimeEnd;
							RangeSelect.Thread = nLog;
							RangeSelect.Index = Index;
							MoveTo(fResultTimeBegin,fResultTimeEnd);
							return;
						}
					}
				}
			}
		}
	}
}

function MoveTo(fMoveBegin, fMoveEnd, YTop, YBottom)
{
	var nOffsetYBottom = YBottom - nHeight;
	var nOffsetYDest = nOffsetY;
	if(nOffsetYDest < nOffsetYBottom)
	{
		nOffsetYDest = nOffsetYBottom;
	}
	if(nOffsetYDest > YTop)
	{	
		nOffsetYDest = YTop;
	}
	var fRange = fDetailedRange;
	var fMinRange = (fMoveEnd-fMoveBegin) * 2.0;
	if(fRange < fMinRange)
	{
		fRange = fMinRange;
	}
	var fMoveCenter = (fMoveBegin + fMoveEnd) * 0.5;
	fMoveBegin = fMoveCenter - 0.5 * fRange;
	fMoveEnd = fMoveCenter + 0.5 * fRange;
	var nOffset;
	if(nOffsetYDest != nOffsetY)
		nOffset = nOffsetYDest;
	ZoomTo(fMoveBegin, fMoveEnd, nOffsetYDest, -1);
}

function ZoomToRange(R)
{
	ZoomTo(R.Begin, R.End, 0, 0, R.Off);
}

let ZoomCallback = null;

function ZoomTo(fZoomBegin, fZoomEnd, OffsetYDest, ZoomTime, fOffset)
{
	if(fZoomBegin < fZoomEnd)
	{
		if(fOffset)
		{
			fZoomBegin -= fOffset;
			fZoomEnd -= fOffset;
		}
		AnimationActive = true;
		var fDetailedOffsetOriginal = fDetailedOffset;
		var fDetailedRangeOriginal = fDetailedRange;
		var fDetailedOffsetTarget = fZoomBegin;
		var fDetailedRangeTarget = fZoomEnd - fZoomBegin;
		var OffsetYOriginal = nOffsetY;
		var OffsetYTarget = OffsetYDest;
		var TimestampStart = new Date();
		var count = 0;
		if(!ZoomTime)
		{			
			ZoomTime = ZOOM_TIME;
		}

		function ZoomFunc()
		{
			ZoomActive = 1;
			var fPrc = (new Date() - TimestampStart) / (ZoomTime * 1000.0);
			if(fPrc > 1.0 || ZoomTime < 0.01)
			{
				fPrc = 1.0;
			}
			fPrc = Math.pow(fPrc, 0.3);
			fDetailedOffset = fDetailedOffsetOriginal + (fDetailedOffsetTarget - fDetailedOffsetOriginal) * fPrc;
			fDetailedRange = fDetailedRangeOriginal + (fDetailedRangeTarget - fDetailedRangeOriginal) * fPrc;
			if(OffsetYDest)
			{
				nOffsetY = OffsetYOriginal + (OffsetYTarget - OffsetYOriginal) * fPrc;
			}
			if(fPrc >= 1.0)
			{
				AnimationActive = false;
				fDetailedOffset = fDetailedOffsetTarget;
				fDetailedRange = fDetailedRangeTarget;
				if(OffsetYDest)
				{
					nOffsetY = OffsetYTarget;
				}
				ZoomCallback = null;
			}
			else
			{
				RequestRedraw();
			}
			return 1;
		}
		ZoomCallback = ZoomFunc;
		RequestRedraw();
	}
}

function RequestAnimationFrame(cb)
{
	if(!RedrawRequested)
	{
		let RedrawCallback = function(foo)
		{
			RedrawRequested = 0;
			cb(foo);
		};
		RedrawRequested = 1;
		requestAnimationFrame(RedrawCallback);
	}
}

function RequestRedraw(FullRedraw)
{
	if(!RedrawRequested)
	{
		RequestAnimationFrame(Draw);
	}
}

function Draw()
{
	let RedrawMode = 1;
	let RedrawAgain = 0;
	RedrawMode = 1;
	if(Mode == ModeDetailed)
	{
		if(ProfileMode == 2 || ((nHoverCSCpu >= 0 || nHoverToken != -1) && !KeyCtrlDown && !KeyShiftDown && !MouseDragButton)||(Invalidate<2 && !KeyCtrlDown && !KeyShiftDown && !MouseDragButton))
		{
			RedrawMode = 1;
			RedrawAgain = 1;
		}
	}
	else
	{
		if(Invalidate < 1)
		{
			RedrawMode = 1;
		}
	}

	if(!Initialized)
	{
		return;
	}
	if(InsideDraw)
	{
		debugger;
		return;
	}
	let ZoomActive = 0;
	if(ZoomCallback)
	{
		ZoomActive = ZoomCallback();
	}
	InsideDraw = 1;
	if(ProfileMode)
	{
		ProfileModeClear();
		ProfileEnter("Total");
	}

	if(ZoomActive)
	{
		DrawDetailed(true);
	}
	else if(RedrawMode == 1)
	{
		if(Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
		{
			DrawBarView();
			DrawHoverToolTip();
		}
		else if(Mode == ModeDetailed)
		{
			DrawDetailed(false);
			DrawHoverToolTip();
		}
		else if(Mode == ModeCounters)
		{
			DrawCounterView();
			DrawHoverToolTip();
		}
	}
	DrawDetailedFrameHistory();
	DrawMenu();


	if(ProfileMode)
	{
		ProfileLeave();
		ProfileModeDraw(CanvasDetailedView);
	}

	InsideDraw = 0;
	if(RedrawAgain)
	{
		RequestRedraw();
	}
	MouseReleased = false;
}

function MoveFilterInputMenuDiv(x, y, w)
{
	if(FilterInputMenuDivPos.x != x || FilterInputMenuDivPos.y != y || FilterInputMenuDivPos.w != w)
	{
		FilterInputMenuDivPos.x = x;
		FilterInputMenuDivPos.y = y;
		FilterInputMenuDivPos.w = w;
		FilterInputMenuDiv.style['left'] = x + 'px';
		FilterInputMenuDiv.style['top'] = y + 'px';
		FilterInputMenu.style['width'] = w + 'px';
	}
}
function MoveFilterInputDiv(x, y, w)
{
	if(FilterInputDivPos.x != x || FilterInputDivPos.y != y || FilterInputDivPos.w != w)
	{
		FilterInputDivPos.x = x;
		FilterInputDivPos.y = y;
		FilterInputDivPos.w = w;
		FilterInputDiv.style['left'] = x + 'px';
		FilterInputDiv.style['top'] = (y + (CanvasHistory.height / DPR)) + 'px';
		FilterInput.style['width'] = w + 'px';
	}
}


function MakeMenuItem(name, f, visible)
{
	var Item = {};
	Item.name = name;
	Item.f = f;
	Item.w = name.length;
	Item.x = 0;
	Item.y = 0;
	Item.visible = visible;
	return Item;
}
function EnableMenu(m)
{
	if(m != SubMenuActive)
	{
		if(SubMenuActive == SubMenuThreads)
		{
			FilterInputMenuThreadsValue = FilterInputMenu.value;
		}
		else if(SubMenuActive == SubMenuGroups)
		{
			FilterInputMenuGroupsValue = FilterInputMenu.value;
		}

		SubMenuActive = m;
		SubMenuTimeout = new Date();

		if(SubMenuActive == SubMenuThreads)
		{
			FilterInputMenu.value = FilterInputMenuThreadsValue;
			FilterInputMenu.focus();
		}
		else if(SubMenuActive == SubMenuGroups)
		{
			FilterInputMenu.value = FilterInputMenuGroupsValue;
			FilterInputMenu.focus();
		}
		FilterInputMenuValueLast = FilterInput.value;
		// if(m == SubMenuHelp)
		{
			let info = document.getElementById("divFrameInfo");
			info.display = 'inline';
		}
	}
	if(m == -1)
	{
		SubMenuTimeout = 0;
	}
	if(SubMenuActive == SubMenuGroups || SubMenuActive == SubMenuThreads)
	{
		FilterInputMenuDiv.style['display'] = 'inline';
		FilterInputMenu.focus();
	}
	else
	{
		FilterInputMenuDiv.style['display'] = 'none';
	}
}
function InitMenu()
{
	MenuItems = [];
	MenuItems.push(MakeMenuItem("?", function(){EnableMenu(SubMenuHelp); } ));
	MenuItems.push(MakeMenuItem("Mode", function(){EnableMenu(SubMenuMode); } ));
	MenuItems.push(MakeMenuItem("Reference", function(){EnableMenu(SubMenuReference); } ));
	MenuItems.push(MakeMenuItem("Target", function(){EnableMenu(SubMenuTarget); } ));
	MenuItems.push(MakeMenuItem("Threads", function(){ EnableMenu(SubMenuThreads); }, function(){ return Mode != ModeCounters && Mode != ModeTimers_Groups; }));
	MenuItems.push(MakeMenuItem("Groups", function(){ EnableMenu(SubMenuGroups); }, function() { return Mode != ModeDetailed && Mode != ModeCounters && Mode != ModeTimers_Threads; } ));
	MenuItems.push(MakeMenuItem("Columns", function(){ EnableMenu(SubMenuColumns); }, function(){return Mode == ModeTimers; }  ));
	MenuItems.push(MakeMenuItem("Options", function(){ EnableMenu(SubMenuOptions); } ));
	MenuItems.push(MakeMenuItem("Compare", function(){ EnableMenu(SubMenuCompare); } ));
}
function DrawTopMenu(context)
{
	MenuItems[SubMenuMode].name = "Mode[" + ModeItems[Mode] + "]";
	let X = 2;
	let Y = 0;
	let MouseInY = GlobalMouseY < BoxHeight;
	for(let i = 0; i < MenuItems.length; ++i)
	{
		let Item = MenuItems[i];
		if(Item.visible == null || Item.visible())
		{
			let w = context.measureText(Item.name).width + 4;
			let MouseIn = MouseInY && GlobalMouseX >= X && GlobalMouseX < X + w;
			let color = MouseIn ? nBackColors[1] : "black";
			Item.x = X;
			Item.y = Y + BoxHeight;
			if(MouseIn)
			{
				context.fillStyle = 'white';
				context.fillRect(X-2, Y, w+4, BoxHeight);
				// Enable
				EnableMenu(i);
			}
			context.fillStyle = color;
			context.fillRect(X, Y, w, BoxHeight);
			context.fillStyle = "white";
			context.fillText(Item.name, X+2, Y+BoxHeight-FontAscent);
			if(MouseIn && MouseReleased)
			{
				Item.f();
			}
			X += w + 6;
		}
	}

	return WindowRect(0, 0, X, BoxHeight);
}
function MenuSize(w)
{
	return WindowRect(nWidth / 2 - w / 2, HistoryHeight + 50,w, nHeight);
}
function MouseInRect(Rect)
{
	return MouseInside(Rect.x, Rect.y, Rect.w, Rect.h);
}
function MouseInside(X, Y, W, H)
{
	return GlobalMouseX >= X && GlobalMouseX <= X + W && GlobalMouseY >= Y && GlobalMouseY <= Y + H;
}

function DrawMenuGeneric(Elements, Active, OnClick, x, y, Elements2)
{
	let context = CanvasMenu.getContext('2d');
	let nColorIndex = 0;
	if(Elements2 && Elements2.length != Elements.length)
	{
		Elements2 = null;
	}
	let h = FontHeight * Elements.length;
	let w = 20;
	let w2 = 0;
	for(let i = 0; i < Elements.length; ++i)
	{
		let m = context.measureText(Elements[i]).width;
		w = w > m ? w : m;
		if(Elements2)
		{
			m = context.measureText(Elements2[i]).width;
			w2 = w2 > m ? w2 : m;
		}
	}
	w += 10 + w2;
	let SizeInfo = MenuSize(w);
	SizeInfo.x = x;
	SizeInfo.y = y;
	let X = x;
	let Y = y;


	for(let i = 0; i < Elements.length; ++i)
	{
		let Selected = Active(i);
		let Name = Elements[i];
		let bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight;
		let bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		let TextY = Y+BoxHeight-FontAscent;
		context.fillStyle = Selected?'white':bgcolor;
		context.fillRect(X-2, Y, w+4, BoxHeight);
		context.fillStyle = bgcolor;
		context.fillRect(X, Y, w, BoxHeight);
		context.fillStyle = 'white';
		context.fillText(Name, X + 2, TextY);
		if(Elements2)
		{
			context.textAlign = "right";
			context.fillText(Elements2[i], X + w , TextY);
			context.textAlign = "left";
		}
		context.fillText(Name, X + 2, TextY);
		if(bMouseIn && MouseReleased)
		{
			OnClick(i, Name);
		}
		Y += BoxHeight;
		nColorIndex = 1-nColorIndex;
	}
	SizeInfo.h = Y - SizeInfo.y;
	return SizeInfo;
}

function DrawMenuReference()
{
	let Names = [];
	let ActiveIdx = 0;
	for(let i in ReferenceTimes)
	{
		let v = ReferenceTimes[i];
		if(v < 0)
		{
			Names.push(ReferenceTimeAutoString);
		}
		else
		{
			if(ReferenceTime == ReferenceTimes[i])
			{
				ActiveIdx = i;
			}
			Names.push(v + "ms");
		}
	}
	let Click = function(idx, name)
	{
		SetReferenceTime(Names[idx]);
		RequestRedraw();
		Invalidate = 0;

	};
	let x = MenuItems[SubMenuReference].x;
	let y = MenuItems[SubMenuReference].y;
	let Active = function(Idx) { return ActiveIdx == Idx; };
	return DrawMenuGeneric(Names, Active, Click, x, y, null);
}
function DrawMenuTarget()
{
	let Names = [];
	for(let i in TargetTimes)
	{
		Names.push(TargetTimes[i] + "ms");
	}
	let Click = function(idx, name)
	{
		SetTargetTime(Names[idx]);
		RequestRedraw();
		Invalidate = 0;

	};
	let x = MenuItems[SubMenuTarget].x;
	let y = MenuItems[SubMenuTarget].y;
	let Active = function(Idx) { return TargetTimes[Idx] == TargetTime; };
	return DrawMenuGeneric(Names, Active, Click, x, y, null);
}

function DrawMenuMode()
{
	let Click = function(idx, name)
	{
		SetMode(idx,false);
		MenuItems[SubMenuMode].name = "Mode[" + ModeItems[idx] + "]";
		RequestRedraw();
		Invalidate = 0;

	};
	let x = MenuItems[SubMenuMode].x;
	let y = MenuItems[SubMenuMode].y;
	let Active = function(Idx) { return Idx == Mode; };
	return DrawMenuGeneric(ModeItems, Active, Click, x, y, null);
}
function DrawMenuOptions()
{
	let OptionNames =["Context Switch", "MergeDisable", "LodDisable", "Flame Mode", "Compare Reverse", "Help"];
	let Click = function(idx, name)
	{
		switch(idx)
		{
			case 0: ToggleContextSwitch(); break;
			case 1: ToggleDisableMerge(); break;
			case 2: ToggleDisableLod(); break;
			case 3: ToggleDetailedFlameMode(); break;
			case 4: ToggleDetailedSecondReverse(); break;
			case 5: ShowHelp(1,1); break;
		}
		RequestRedraw();
		Invalidate = 0;

	};
	let x = MenuItems[SubMenuOptions].x;
	let y = MenuItems[SubMenuOptions].y;
	let Active = function(Idx) { return false; };
	return DrawMenuGeneric(OptionNames, Active, Click, x, y, null);
}

function DrawMenuColumns()
{
	let Click = function(idx, name)
	{
		ToggleColumn(idx);

	};
	let x = MenuItems[SubMenuColumns].x;
	let y = MenuItems[SubMenuColumns].y;
	let Active = function(Idx) { return ColumnsEnabled[Idx]; };
	return DrawMenuGeneric(ColumnNames, Active, Click, x, y, null);

}

function GroupMenuSize()
{
	return MenuSize(300);
}

function ThreadMenuSize()
{
	return MenuSize(S.ThreadNameWidth + S.ThreadCategoryWidth + 10);
}

function DrawMultiMenu(context, X, Y, W, Elements, Callbacks)
{
	let A = Array();
	let wtemp = W;
	let bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight;

	for(let i = 0; i < Elements.length; ++i)
	{
		let wElement = context.measureText(Elements[i]).width;
		if(i > 0)
		{
			wElement += 15;
			wtemp -= wElement;
		}
		A.push(wElement);
	}
	A[0] = Math.max(wtemp, 0);

	context.fillStyle = nBackColors[0];
	context.fillRect(X-2, Y, W, BoxHeight);
	let XOff = 0;
	let TextY = Y+BoxHeight-FontAscent;
	for(let i = 0; i < Elements.length; ++i)
	{
		let width = A[i];
		let Inside = i != 0 && bMouseIn && GlobalMouseX > XOff + X && GlobalMouseX <= XOff + X + width;
		context.fillStyle = Inside ? nBackColorOffset : nBackColors[0];
		context.fillRect(X+XOff, Y, width, BoxHeight);
		context.fillStyle = 'white';
		context.textAlign = 'center';
		context.fillText(Elements[i], X+XOff + width * 0.5, TextY);
		XOff += width;
		if(Inside && MouseReleased && Callbacks[i])
		{
			Callbacks[i]();
		}
	}
	context.textAlign = 'left';
}

function DrawMenuThreads()
{
	if(FilterInputMenuValueLast != FilterInputMenu.value)
	{
		nOffsetMenuThreads = 0;
	}
	FilterInputMenuValueLast = FilterInputMenu.value;
	let FilterArray = CreateFilter(FilterInputMenu.value);
	let context = CanvasMenu.getContext('2d');
	let nColorIndex = 0;
	let SizeInfo = ThreadMenuSize();
	SizeInfo.x = MenuItems[SubMenuThreads].x;
	SizeInfo.y = MenuItems[SubMenuThreads].y;
	let Y = SizeInfo.y;
	let Width = SizeInfo.w;
	let Selection = null;
	let X = SizeInfo.x;
	MoveFilterInputMenuDiv(SizeInfo.x, SizeInfo.y, SizeInfo.w);
	Y += 35;

	let bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight;
	let bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	let TextY = Y+BoxHeight-FontAscent;
	let YClear = Y;
	let TextYClear = TextY;
	let YStart = Y;
	let MatchCount = 0;
	let MouseTaken = bMouseIn;

	Y += BoxHeight * 3;
	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuThreads;

	for(let i = 0; i < S.ThreadNames.length; ++i)
	{
		let Name = S.ThreadNames[i];
		let ParentName = "ThreadCategory";
		if(FilterMatch(FilterArray, ParentName + " " + Name))
		{
			if(Y > YStart)
			{
				var ParentColor = 'white';
				let E =	ThreadsActive[Name];
				let AutoHidden = S.ThreadLogAutoHidden[i];
				bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight && !MouseTaken;
				bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
				TextY = Y+BoxHeight-FontAscent;
				context.fillStyle = E ? (AutoHidden?'orange':'white') :bgcolor;
				context.fillRect(X-2, Y, Width+4, BoxHeight);
				context.fillStyle = bgcolor;
				context.fillRect(X, Y, Width, BoxHeight);
				context.fillStyle = ParentColor;
				context.fillText(ParentName, X + 2, TextY);
				context.fillStyle = S.ThreadColors[i].color;
				context.textAlign = 'right';
				context.fillText(Name, X + Width - 2, TextY);
				context.textAlign = 'left';
				if(bMouseIn && MouseReleased)
				{
					ToggleThread(Name);
					RequestRedraw();
				}
			}
			Y += BoxHeight;
			nColorIndex = 1-nColorIndex;
			MatchCount++;
		}
	}
	let TextAll = "All [" + S.ThreadNames.length + "]";
	let TextFiltered = "Filtered [" + MatchCount +"]";



	let ElementsAll = [TextAll, "Off", "Flip", "On"];
	let CallbacksAll = [null, 
		function(){ ToggleThread(0, 1, 0, 0); },
		function(){ ToggleThread(0, 1, 0, 1); },
		function(){ ToggleThread(0, 1, 0, 2); },
	];
	let ElementsFiltered = [TextFiltered, "Off", "Flip", "On"];
	let CreateFilteredArray = function()
	{
		if(!FilterArray) return S.ThreadNames;
		let A = [];
		for(let i = 0; i < S.ThreadNames.length; ++i)
		{
			let Name = S.ThreadNames[i];
			let ParentName = "ThreadCategory";
			if(FilterMatch(FilterArray, ParentName + " " + Name))
			{
				A.push(Name);
			}
		}
		return A;
	};
	let CallbacksFiltered = [null, 
		function(){let F = CreateFilteredArray(); ToggleThread(0, 0, F, 0); },
		function(){let F = CreateFilteredArray(); ToggleThread(0, 0, F, 1); },
		function(){let F = CreateFilteredArray(); ToggleThread(0, 0, F, 2); },
	];


	let ElementsOptions = ["Options", "Hide mode:" + (HideMode ? "Collapsed" : "Invisible"), "AutoHide Empty:" + (ThreadLogAutoHide ? "On": "Off")];
	let CallbacksOptions = [null,
	function(){ 
		HideMode = !HideMode; 
		RequestRedraw();
		Invalidate = 0;
	}, 
	function(){
		ThreadLogAutoHide = ThreadLogAutoHide ? 0 : 1; UpdateThreadLogAutoHide()
		RequestRedraw();
		Invalidate = 0;
	}
	];

	DrawMultiMenu(context, X, YClear, Width, ElementsOptions, CallbacksOptions);
	YClear += BoxHeight;
	DrawMultiMenu(context, X, YClear, Width, ElementsAll, CallbacksAll);
	YClear += BoxHeight;
	DrawMultiMenu(context, X, YClear, Width, ElementsFiltered, CallbacksFiltered);

	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;
}

function DrawMenuGroups()
{
	if(FilterInputMenuValueLast != FilterInputMenu.value)
	{
		nOffsetMenuThreads = 0;
	}
	FilterInputMenuValueLast = FilterInputMenu.value;
	let FilterArray = CreateFilter(FilterInputMenu.value);
	let context = CanvasMenu.getContext('2d');
	let nColorIndex = 0;
	let SizeInfo = ThreadMenuSize();
	SizeInfo.x = MenuItems[SubMenuGroups].x;
	SizeInfo.y = MenuItems[SubMenuGroups].y;
	let Y = SizeInfo.y;
	let Width = SizeInfo.w;
	let Selection = null;
	let X = SizeInfo.x;
	MoveFilterInputMenuDiv(SizeInfo.x, SizeInfo.y, SizeInfo.w);
	Y += 35;

	let bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight;
	let bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	let TextY = Y+BoxHeight-FontAscent;
	let YClear = Y;
	let TextYClear = TextY;
	let YStart = Y;
	let MatchCount = 0;
	let MouseTaken = bMouseIn;

	Y += BoxHeight * 2;
	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuThreads;



	for(var i = 0; i < S.GroupInfo.length; ++i)
	{
		let Name = S.GroupInfo[i].name;
		let ParentName = S.CategoryInfo[S.GroupInfo[i].category];
		let Color = g_Colors[ S.GroupInfo[i].cid ];
		if(FilterMatch(FilterArray, ParentName + " " + Name))
		{
			if(Y > YStart)
			{
				var ParentColor = 'white';
				let E =	!GroupsDisabled[Name];
				bMouseIn = GlobalMouseY >= Y && GlobalMouseY < Y + BoxHeight && !MouseTaken;
				bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
				TextY = Y+BoxHeight-FontAscent;
				context.fillStyle = E ? 'white' :bgcolor;
				context.fillRect(X-2, Y, Width+4, BoxHeight);
				context.fillStyle = bgcolor;
				context.fillRect(X, Y, Width, BoxHeight);
				context.fillStyle = ParentColor;
				context.fillText(ParentName, X + 2, TextY);
				context.fillStyle = Color;
				context.textAlign = 'right';
				context.fillText(Name, X + Width - 2, TextY);
				context.textAlign = 'left';
				if(bMouseIn && MouseReleased)
				{
					ToggleGroup(Name);
					RequestRedraw();
				}
			}
			Y += BoxHeight;
			nColorIndex = 1-nColorIndex;
			MatchCount++;
		}
	}
	let TextAll = "All [" + S.GroupInfo.length + "]";
	let TextFiltered = "Filtered [" + MatchCount +"]";



	let ElementsAll = [TextAll, "Off", "Flip", "On"];
	let CallbacksAll = [null, 
		function(){ ToggleGroup(0, 1, 0, 0); },
		function(){ ToggleGroup(0, 1, 0, 1); },
		function(){ ToggleGroup(0, 1, 0, 2); },
	];
	let ElementsFiltered = [TextFiltered, "Off", "Flip", "On"];
	let CreateFilteredArray = function()
	{
		let A = [];
		for(let i = 0; i < S.GroupInfo.length; ++i)
		{
			let Name = S.GroupInfo[i].name;
			let ParentName = S.CategoryInfo[S.GroupInfo[i].category];
			if(FilterMatch(FilterArray, ParentName + " " + Name))
			{
				A.push(Name);
			}
		}
		return A;
	};
	let CallbacksFiltered = [null, 
		function(){let F = CreateFilteredArray(); ToggleGroup(0, 0, F, 0); },
		function(){let F = CreateFilteredArray(); ToggleGroup(0, 0, F, 1); },
		function(){let F = CreateFilteredArray(); ToggleGroup(0, 0, F, 2); },
	];


	DrawMultiMenu(context, X, YClear, Width, ElementsAll, CallbacksAll);
	DrawMultiMenu(context, X, YClear+BoxHeight, Width, ElementsFiltered, CallbacksFiltered);

	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;


}

function DrawMenu()
{
	MenuRedraw = 0;

	var context = CanvasMenu.getContext('2d');
	context.clearRect(0, 0, CanvasMenu.width, CanvasMenu.height);

	var nColorIndex = 0;
	var Y = 50;
	var Width = 300;
	var Selection = null;

	ProfileEnter("DrawMenu");
	let MenuRect = DrawTopMenu(context);
	if(SubMenuActive != -1)
	{
		MouseMoveTime = new Date();
	}
	if(SubMenuActive == SubMenuHelp)
	{
		if(MouseReleased)
		{
			ToggleDebugMode();
		}
	}
	if(SubMenuActive == SubMenuMode)
	{
		MenuRect = DrawMenuMode();
	}
	else if(SubMenuActive == SubMenuReference)
	{
		MenuRect = DrawMenuReference();
	}
	else if(SubMenuActive == SubMenuTarget)
	{
		MenuRect = DrawMenuTarget();
	}
	else if(SubMenuActive == SubMenuThreads)
	{
		MenuRect = DrawMenuThreads();
	}
	else if(SubMenuActive == SubMenuGroups)
	{
		MenuRect = DrawMenuGroups();
	}
	else if(SubMenuActive == SubMenuOptions)
	{
		MenuRect = DrawMenuOptions();
	}
	else if(SubMenuActive == SubMenuCompare)
	{
		if(MouseReleased)
		{
			ComparePrompt();
		}
	}
	else if(SubMenuActive == SubMenuColumns)
	{
		MenuRect = DrawMenuColumns();
	}
	var Grow = 10;
	MenuRect.x -= Grow;
	MenuRect.y -= Grow;
	MenuRect.h += 2*Grow;
	MenuRect.w += 2*Grow;
	var MouseMoved = GlobalMouseX != SubMenuMouseX || GlobalMouseY != SubMenuMouseY;

	if(MouseInRect(MenuRect) || !MouseMoved)
	{
		SubMenuTimeout = new Date();
		SubMenuMouseX = GlobalMouseX;
		SubMenuMouseY = GlobalMouseY;
	}
	else
	{
		var Time = new Date() - SubMenuTimeout;
		var Dest = SubMenuTimeoutBase * 1000;
		if(Time > Dest)
		{
			EnableMenu(-1);
		}
	}
	if(0)//debugging of menu extents. dont delete
	{
		context.strokeStyle = 'red';
		context.beginPath();
		context.moveTo(MenuRect.x,MenuRect.y);
		context.lineTo(MenuRect.x + MenuRect.w,MenuRect.y);
		context.lineTo(MenuRect.x + MenuRect.w,MenuRect.y+MenuRect.h);
		context.lineTo(MenuRect.x,MenuRect.y+MenuRect.h);
		context.lineTo(MenuRect.x,MenuRect.y);
		context.stroke();
	}	// SpinnerDraw(SpinnerShow(), context, SpinnerCorner, 0, nHeight-20,  20, 20);
	ProfileLeave();
}


function ZoomGraph(nZoom)
{
	var fOldRange = fDetailedRange;
	if(nZoom>0)
	{
		fDetailedRange *= Math.pow(nModDown ? 1.40 : 1.03, nZoom);
	}
	else
	{
		var fNewDetailedRange = fDetailedRange / Math.pow((nModDown ? 1.40 : 1.03), -nZoom);
		if(fNewDetailedRange < 0.0001) //100ns
			fNewDetailedRange = 0.0001;
		fDetailedRange = fNewDetailedRange;
	}

	var fDiff = fOldRange - fDetailedRange;
	var fMousePrc = DetailedViewMouseX / nWidth;
	if(fMousePrc < 0)
	{
		fMousePrc = 0;
	}
	fDetailedOffset += fDiff * fMousePrc;

}

function MeasureFont()
{
	var context = CanvasDetailedView.getContext('2d');
	context.font = Font;
	FontWidth = context.measureText('W').width;

}
function ResizeCanvas() 
{
	nWidth = window.innerWidth;
	nHeight = window.innerHeight - CanvasHistory.height-2;
	DPR = window.devicePixelRatio;

	if(DPR)
	{
		CanvasDetailedView.style.width = nWidth + 'px'; 
		CanvasDetailedView.style.height = nHeight + 'px';
		CanvasDetailedView.width = nWidth * DPR;
		CanvasDetailedView.height = nHeight * DPR;
		CanvasHistory.style.width = window.innerWidth + 'px';
		CanvasHistory.style.height = 70 + 'px';
		CanvasHistory.width = window.innerWidth * DPR;
		CanvasHistory.height = 70 * DPR;
		CanvasMenu.style.width = window.innerWidth + 'px';
		CanvasMenu.style.height = window.innerHeight + 'px';
		CanvasMenu.width = window.innerWidth * DPR;
		CanvasMenu.height = window.innerHeight * DPR;
		CanvasHistory.getContext('2d').scale(DPR,DPR);
		CanvasDetailedView.getContext('2d').scale(DPR,DPR);
		CanvasMenu.getContext('2d').scale(DPR,DPR);

		CanvasDetailedOffscreen.style.width = nWidth + 'px';
		CanvasDetailedOffscreen.style.height = nHeight + 'px';
		CanvasDetailedOffscreen.width = nWidth * DPR;
		CanvasDetailedOffscreen.height = nHeight * DPR;
		CanvasDetailedOffscreen.getContext('2d').scale(DPR,DPR);

	}
	else
	{
		DPR = 1;
		CanvasDetailedView.width = nWidth;
		CanvasDetailedView.height = nHeight;
		CanvasDetailedOffscreen.width = nWidth;
		CanvasDetailedOffscreen.height = nHeight;

		CanvasMenu.width = window.innerWidth;
		CanvasMenu.height = window.innerHeight;
		CanvasHistory.width = window.innerWidth;
	}
	RequestRedraw();
}


var MouseDragOff = 0;
var MouseDragDown = 1;
var MouseDragUp = 2;
var MouseDragMove = 3;
var MouseDragState = MouseDragOff;
var MouseDragTarget = 0;
var MouseDragButton = 0;
var MouseDragButtonNext = 0;
var MouseDragKeyShift = 0;
var MouseDragKeyCtrl = 0;
var MouseDragKeyAlt = 0;
var MouseDragX = 0;
var MouseDragY = 0;
var MouseDragXLast = 0;
var MouseDragYLast = 0;
var MouseDragXStart = 0;
var MouseDragYStart = 0;

function clamp(number, min, max)
{
  return Math.max(min, Math.min(number, max));
}

function MouseDragPan()
{
	return MouseDragButton == 1 || MouseDragKeyShift || MouseDragKeyAlt;
}
function MouseDragSelectRange()
{
	return MouseDragState == MouseDragMove && (MouseDragButton == 3 || (MouseDragKeyShift && MouseDragKeyCtrl));
}
function MouseHandleDrag()
{
	if(MouseDragTarget == CanvasDetailedView)
	{
		if(Mode == ModeDetailed)
		{

			if(FilterSearchActive)
			{
				if(MouseDragKeyShift || MouseDragButton == 1)
				{
					var Y = MouseDragY - MouseDragYLast;
					nOffsetFilterSearch -= Y;
					if(nOffsetFilterSearch < 0)
					{
						nOffsetFilterSearch = 0;
					}
				}
			}
			else
			{
				if(MouseDragSelectRange())
				{
					var xStart = MouseDragXStart;
					var xEnd = MouseDragX;
					if(xStart > xEnd)
					{
						var Temp = xStart;
						xStart = xEnd;
						xEnd = Temp;
					}
					if(xEnd - xStart > 1)
					{
						RangeCpu.Begin = fDetailedOffset + fDetailedRange * (xStart / nWidth);
						RangeCpu.End = fDetailedOffset + fDetailedRange * (xEnd / nWidth);
						RangeSelect.Begin = fDetailedOffset + fDetailedRange * (xStart / nWidth);
						RangeSelect.End = fDetailedOffset + fDetailedRange * (xEnd / nWidth);
						RangeSelect.Thread = -1;
						RangeSelect.Index = -1;
					}
				}
				else if(MouseDragPan())
				{
					var X = MouseDragX - MouseDragXLast;
					var Y = MouseDragY - MouseDragYLast;
					if(X)
					{
						if(MouseDragKeyAlt)
						{
							fDetailedOffsetSecond += -X * fDetailedRange / nWidth;
						}
						else
						{
							fDetailedOffset += -X * fDetailedRange / nWidth;
						}
					}
					if(!MouseDragKeyAlt)
					{						
						nOffsetY -= Y;
					}
					if(nOffsetY < 0)
					{
						nOffsetY = 0;
					}
				}
				else if(MouseDragKeyCtrl)
				{
					if(MouseDragY != MouseDragYLast)
					{
						ZoomGraph(MouseDragY - MouseDragYLast);
					}
				}
			}	
		}
		else if(Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
		{
			if(MouseDragKeyShift || MouseDragButton == 1)
			{
				var X = MouseDragX - MouseDragXLast;
				var Y = MouseDragY - MouseDragYLast;
				nOffsetBarsY -= Y;
				nOffsetBarsX -= X;
				if(nOffsetBarsY < 0)
				{
					nOffsetBarsY = 0;
				}
				if(nOffsetBarsX < 0)
				{
					nOffsetBarsX = 0;
				}
			}
		}
		else if(Mode == ModeCounters)
		{
			if(MouseDragKeyShift || MouseDragButton == 1)
			{
				var Y = MouseDragY - MouseDragYLast;
				nOffsetCountersY -= Y;
				if(nOffsetCountersY < 0)
				{
					nOffsetCountersY = 0;
				}
			}
		}

	}
	else if(MouseDragTarget == CanvasHistory)
	{
		function HistoryFrameTime(x)
		{
			var NumFrames = S.Frames.length;
			var fBarWidth = nWidth / NumFrames;
			var Index = clamp(Math.floor(NumFrames * x / nWidth), 0, NumFrames-1);
			var Lerp = clamp((x/fBarWidth - Index) , 0, 1);
			var time = S.Frames[Index].framestart + (S.Frames[Index].frameend - S.Frames[Index].framestart) * Lerp;
			return time;
		}
		if(MouseDragSelectRange())
		{
			RangeCpu = RangeInit();
			RangeGpu = RangeInit();

			var xStart = MouseDragXStart;
			var xEnd = MouseDragX;
			if(xStart > xEnd)
			{
				var Temp = xStart;
				xStart = xEnd;
				xEnd = Temp;
			}
			if(xEnd - xStart > 2)
			{
				var timestart = HistoryFrameTime(xStart);
				var timeend = HistoryFrameTime(xEnd);
				fDetailedOffset = timestart;
				fDetailedRange = timeend-timestart;
			}
		}
		else if(MouseDragPan())
		{
			var Time = HistoryFrameTime(MouseDragX);
			fDetailedOffset = Time - fDetailedRange / 2.0;
		}
	}
}
function MouseHandleDragEnd()
{
	if(MouseDragTarget == CanvasDetailedView)
	{

	}
	else if(MouseDragTarget == CanvasHistory)
	{
		if(!MouseDragSelectRange() && !MouseDragPan())
		{
			ZoomToHighlight(1);
		}
	}
}

function MouseHandleDragClick()
{
	if(MouseDragTarget == CanvasDetailedView)
	{
		if(Mode == ModeCounters)
		{
			if(nHoverCounter != -1)
			{
				if(S.CounterInfo[nHoverCounter].firstchild != -1)
				{
					S.CounterInfo[nHoverCounter].closed = !S.CounterInfo[nHoverCounter].closed;
				}
				else
				{
					S.CounterInfo[nHoverCounter].Expanded = !S.CounterInfo[nHoverCounter].Expanded;
				}
				Draw(1);
			}
		}
		else if(Mode == ModeDetailed && FilterSearchSelection >= 0)
		{
			FilterInputCommit();
		}
		else
		{
			if(DetailedMouseEvent)
			{
				DetailedMouseEvent.E();
			}
			else if(!DetailedMouseOverButton)
			{
				ZoomToHighlight();
			}
		}
	}
	else if(MouseDragTarget == CanvasHistory)
	{
		if(Mode == ModeDetailed)
		{
			ZoomToHighlight(1);
		}
	}
}

function MapMouseButton(event)
{
	if(event.button == 1 || event.which == 1)
	{
		return 1;
	}
	else if(event.button == 3 || event.which == 3)
	{
		return 3;
	}
	else
	{
		return 0;
	}
}

function MouseDragIsActive()
{
	return MouseDragState != MouseDragOff && MouseDragState != MouseDragDown;
}
function MouseDragReset()
{
	MouseDragState = MouseDragOff;
	MouseDragTarget = 0;
	MouseDragKeyShift = 0;
	MouseDragKeyCtrl = 0;
	MouseDragKeyAlt = 0;
	MouseDragButton = 0;
}
function MouseDragKeyUp()
{
	if((MouseDragKeyShift && !KeyShiftDown) || (MouseDragKeyCtrl && !KeyCtrlDown) || (MouseDragKeyAlt && !KeyAltDown) || SubMenuActive != -1)
	{
		MouseHandleDragEnd();
		MouseDragReset();
	}
}
function MouseDrag(Source, Event)
{
	if(Source == MouseDragOff || (MouseDragTarget && MouseDragTarget != Event.target) || SubMenuActive != -1)
	{
		MouseDragReset();
		return;
	}
	if(G_DEBUG)
	{
		debugger;
	}
	var LocalRect = Event.target.getBoundingClientRect();
	MouseDragX = Event.clientX - LocalRect.left;
	MouseDragY = Event.clientY - LocalRect.top;
	if(MouseDragState == MouseDragMove)
	{
		var dx = Math.abs(MouseDragX - MouseDragXStart);
		var dy = Math.abs(MouseDragY - MouseDragYStart);
		if((Source == MouseDragUp && MapMouseButton(Event) == MouseDragButton) ||
			(MouseDragKeyCtrl && !KeyCtrlDown) ||
			(MouseDragKeyShift && !KeyShiftDown) ||
			(MouseDragKeyAlt && !KeyAltDown))
		{
			MouseHandleDragEnd();
			MouseDragReset();
			return;
		}
		else
		{
			MouseHandleDrag();
		}
	}
	else if(MouseDragState == MouseDragOff)
	{
		if(Source == MouseDragDown || KeyShiftDown || KeyCtrlDown|| KeyAltDown)
		{
			MouseDragTarget = Event.target;
			MouseDragButton = MapMouseButton(Event);
			MouseDragState = MouseDragDown;
			MouseDragXStart = MouseDragX;
			MouseDragYStart = MouseDragY;
			MouseDragKeyCtrl = 0;
			MouseDragKeyShift = 0;
			MouseDragKeyAlt = 0;

			if(KeyShiftDown || KeyCtrlDown || KeyAltDown)
			{
				MouseDragKeyAlt = KeyAltDown;
				MouseDragKeyShift = KeyShiftDown;
				MouseDragKeyCtrl = KeyCtrlDown;
				MouseDragState = MouseDragMove;
			}
		}
	}
	else if(MouseDragState == MouseDragDown)
	{
		if(Source == MouseDragUp)
		{
			MouseHandleDragClick();
			MouseDragReset();
		}
		else if(Source == MouseDragMove)
		{
			var dx = Math.abs(MouseDragX - MouseDragXStart);
			var dy = Math.abs(MouseDragY - MouseDragYStart);
			if(dx+dy>1)
			{
				MouseDragState = MouseDragMove;
			}
		}
	}
	MouseDragXLast = MouseDragX;
	MouseDragYLast = MouseDragY;
}

function MouseMove(evt)
{
	evt.preventDefault();
	ZoomActive = 0;
	MouseDrag(MouseDragMove, evt);
 	MouseHistory = 0;
	MouseDetailed = 0;
	HistoryViewMouseX = HistoryViewMouseY = -1;
	var rect = evt.target.getBoundingClientRect();
	var x = evt.clientX - rect.left;
	var y = evt.clientY - rect.top;
	GlobalMouseX = evt.pageX;
	GlobalMouseY = evt.pageY;
	if(evt.target == CanvasDetailedView)
	{
		if(!MouseDragSelectRange())
		{
			RangeCpu = RangeInit();
		}
		DetailedViewMouseX = x;
		DetailedViewMouseY = y;
	}
	else if(evt.target = CanvasHistory)
	{
		var Rect = CanvasHistory.getBoundingClientRect();
		HistoryViewMouseX = x;
		HistoryViewMouseY = y;

		DetailedViewMouseX = -1;
		DetailedViewMouseY = -1;

	}
	RequestRedraw();
}

function MouseSortClick()
{
	if(SortColumnMouseOverNext)
	{
		if(SortColumnMouseOverNext == SortColumnMouseOver)
		{
			SortColumnOrderFlip =  1 - SortColumnOrderFlip;
		}
		else
		{
			SortColumnOrderFlip = 0;
		}

		SortColumnMouseOver = SortColumnMouseOverNext;
		SortColumnMouseOverNext = null;
		if(SortColumnMouseOver == StrAverage)
		{
			SortColumn = 1;
		}
		else if(SortColumnMouseOver == StrMax)
		{
			SortColumn = 2;
		}
		else if(SortColumnMouseOver == StrTotal)
		{
			SortColumn = 3;
		}			
		else if(SortColumnMouseOver == StrMin)
		{
			SortColumn = 4;
		}
		else if(SortColumnMouseOver == StrSpike)
		{
			SortColumn = 5;
		}
		else if(SortColumnMouseOver == StrCallAverage)
		{
			SortColumn = 6;
		}
		else if(SortColumnMouseOver == StrCount)
		{
			SortColumn = 7;
		}
		else if(SortColumnMouseOver == StrExclAverage)
		{
			SortColumn = 8;
		}
		else if(SortColumnMouseOver == StrExclMax)
		{
			SortColumn = 9;
		}
		else if(SortColumnMouseOver == StrGroup)
		{
			SortColumn = 0;
		}
		RequestRedraw();
	}
}

function MouseButton(bPressed, evt)
{
	evt.preventDefault();
	MouseReleased = !bPressed;
	MouseDrag(bPressed ? MouseDragDown : MouseDragUp, evt);
	if(!bPressed && SubMenuActive == -1)
		MouseSortClick();
	RequestRedraw();
}

function MouseOut(evt)
{
	MouseDrag(MouseDragOff, evt);
	KeyCtrlDown = 0;
	KeyShiftDown = 0;
	KeyAltDown = 0;
	MouseDragButton = 0;
	nHoverToken = -1;
	RangeCpu = RangeInit();
}

function MouseWheel(e)
{
    var e = window.event || e;
    var delta = (e.wheelDelta || e.detail * (-120));
    ZoomGraph((-4 * delta / 120.0) | 0);
    Draw(1);
}
function ShowFilterInput(bShow)
{
	if(bShow)
	{
		document.getElementById('filterinput').style['display'] = 'block';
	}
	else
	{
		document.getElementById('filterinput').style['display'] = 'none';
	}
}

function SetFilterInput(group, timer)
{
	FilterInputGroupString = group;
	FilterInputTimerString = timer;
	FilterInputGroup.value = group?group:'';
	FilterInputTimer.value = timer?timer:'';
	FilterUpdate();
	if(group || timer)
	{
		ShowFilterInput(1);
	}
	else
	{
		ShowFilterInput(0);
	}

}

function ToggleFilterInput(escape)
{
	var ActiveElement = -1;
	for(var i = 0; i < FilterInputArray.length; ++i)
	{
		if(FilterInputArray[i] == document.activeElement)
		{
			ActiveElement = i;
		}
	}
	var OldActiveElement = ActiveElement;
	if(ActiveElement >= 0)
	{
		FilterInputArray[ActiveElement].blur();
	}
	ActiveElement++;
	if(!escape)
	{
		if(ActiveElement < FilterInputArray.length)
		{
			ShowFilterInput(1);
			FilterInputArray[ActiveElement].focus();
		}
	}
	else
	{
		if(-1 == OldActiveElement)
		{
			SetFilterInput();
		}
	}
}

function KeyUp(evt)
{
	// console.log("keyup ", evt.keyCode);

	if(evt.keyCode== 84)
	{
		G_DEBUG = 1;
	}
	if(evt.keyCode == 18)
	{
		ToolTipFlip = 0;
	}
	// ShowFlashMessage(evt.keyCode + ' <<', 100);
	if(!FilterSearchActive && !IgnoreInput && SubMenuActive == -1)
	{
		if(evt.keyCode == 112)
		{
			ShowHelp(1, 0);
		}
		if(evt.keyCode == 39)
		{
			MoveToNext(1);
		}
		if(evt.keyCode == 37)
		{
			MoveToNext(-1);
		}
		if(evt.keyCode == 90)
		{
			ToolTipCorner = 1-ToolTipCorner;
		}
		if(evt.keyCode == 187)
		{
			DrawDetailedFlameMode = (DrawDetailedFlameMode+1) % 3;
			FilterSearchReset();
		}
		if(evt.keyCode == 189)
		{
			DrawDetailedCompareReverse = 1-DrawDetailedCompareReverse;
			FilterSearchReset();
		}
		if(evt.keyCode == 32)
		{
			if(RangeSelect.Begin < RangeSelect.End)
			{
				ZoomToRange(RangeSelect);
				RangeSelect = RangeInit();
				MouseHandleDragEnd();
			}
		}
		if(evt.keyCode == 9)
		{
			evt.preventDefault();
			if(Mode == ModeDetailed)
			{
				var Token = nHoverToken;
				if(Token == -1 && RangeValid(RangeSelect) && RangeSelect.Index >= 0)
				{
					Token = RangeSelect.Index;
				}
				if(Token != -1 && Token < S.TimerInfo.length)
				{
					var start = S.TimerInfo[Token].worststart;
					var end = S.TimerInfo[Token].worstend;
					RangeSelect.Begin = start;
					RangeSelect.End = end;
					RangeSelect.Thread = S.TimerInfo[Token].worstthread;
					RangeSelect.Index = Token;
					ShowFlashMessage('Worst: ' + (end-start).toFixed(2) + 'ms', 100);
					MoveTo(RangeSelect.Begin, RangeSelect.End, ThreadYBegin[RangeSelect.Thread] + nOffsetY, ThreadYEnd[RangeSelect.Thread+1] + nOffsetY);
					MouseHandleDragEnd();
				}
			}
			else if(Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
			{
				ToggleFilterInput(0);
				evt.preventDefault();
			}

		}
		if(evt.keyCode == 88)
		{
			ToggleMode();
		}

	}
	if(evt.keyCode == 18)
	{
		KeyAltDown = 0;
		MouseDragKeyUp();
	}		
	else if(evt.keyCode == 17)
	{ 
		KeyCtrlDown = 0;
		MouseDragKeyUp();
	}
	else if(evt.keyCode == 16)
	{
		KeyShiftDown = 0;
		MouseDragKeyUp();
	}


	if(evt.keyCode == 27)
	{
		RangeSelect = RangeInit();
		SortColumn = 0;
		SortColumnMouseOver = "";
		if(Mode == ModeTimers || Mode == ModeTimers_Threads || Mode == ModeTimers_Groups)
		{
			ToggleFilterInput(1);
			evt.preventDefault();
		}
		if(SubMenuActive != -1)
		{
			if(FilterInputMenu.value.trim() != "")
			{
				FilterInputMenu.value = "";
			}
			else
			{
				EnableMenu(-1);
			}			
		}

		if(FilterSearchActive)
		{
			if(FilterInput.value != '')
			{
				FilterInput.value = '';
			}
			else
			{
				FilterInputHide();
			}
		}
		else
		{
			FilterSearchReset();
		}
	}


	if(evt.keyCode == 13 && !IgnoreInput)
	{
		if(FilterSearchActive)
		{
			FilterInputCommit();
		}
		else
		{
			FilterInputShow();
		}
	}
	RequestRedraw();
	Invalidate = 0;
}
function FilterInputUpdate()
{
	Invalidate = 0;	
	if(FilterSearchActive == 1)
	{
		FilterInputDiv.style['display'] = 'inline';
		FilterInput.focus();
	}
	else
	{
		FilterSearchSelection = -1;
		FilterInputDiv.style['display'] = 'none';
	}	
}
function FilterInputShow()
{
	FilterSearchActive = 1;
	FilterInputUpdate();
	RequestRedraw();
}
function FilterInputHide()
{
	FilterSearchActive = 0;
	FilterInputUpdate();
}

function FilterInputCommit()
{
	if(FilterSearchSelection >= 0)
	{
		FilterSearchReset();
		FilterSearchPassIndex = FilterSearchSelection;
		FilterSearchStartTime = new Date();
	}
	FilterInputHide();
}
function FilterSearchReset()
{
	FilterSearchArray = new Array();	
}
function CreateFilter(Filter)
{
	if(!Filter || Filter.length == 0)
	{
		return null;
	}
	Filter = Filter.split(' ');
	
	var regexp = "";
	for(var i = 0; i < Filter.length; ++i)
	{
		regexp = regexp + ".*" + Filter[i];
	}
	Filter = new Array();
	regexp = regexp + ".*";
	Filter.push(new RegExp(regexp, "i"));
	return Filter;
}
function FilterKeyUp()
{
	FilterInputTimerString = FilterInputTimer.value;
	FilterInputGroupString = FilterInputGroup.value;
	FilterUpdate();
}

function FilterUpdate()
{
	FilterTimer = CreateFilter(FilterInputTimerString);
	FilterGroup = CreateFilter(FilterInputGroupString);
}

function KeyDown(evt)
{
	// console.log("keydow ", evt.keyCode);

	if(evt.keyCode == 18)
	{
		KeyAltDown = 1;
	}
	else if(evt.keyCode == 17)
	{
		KeyCtrlDown = 1;
	}
	else if(evt.keyCode == 16)
	{
		KeyShiftDown = 1;
	}
	else if(evt.keyCode == 9)
	{
		evt.preventDefault();
	}
	else if(evt.keyCode == 13)
	{
		evt.preventDefault();
	}
	if(evt.keyCode == 91) // z/tab to toggle tooltip
	{
		ToolTipFlip = 1;
	}


	Invalidate = 0;
}
function ParseCookie(str)
{
	if(!str) return null;
	var result = str.match(/fisk=([^;]+)/);
	// console.log("cookie is " + str);
	if(result && result.length > 0)
	{
		var Obj = JSON.parse(result[1]);
		if(!Obj.offline)
		{
			var C = {};
			C.offline = Obj;
			Obj = C;
		}		
		return Obj;
	}
	return null;
}

function GetCookie()
{
	var Obj = ParseCookie(localStorage.getItem("microprofile_fisk"));
	if(!Obj)
	{
		Obj = ParseCookie(document.cookie);
	}
	if(!Obj)
	{
		return {offline:{},live:{}};
	}
	return Obj;
}

function ReadCookie()
{
	var C = GetCookie();
	var NewMode = ModeDetailed;
	if(C.offline)
	{
		var Obj = C.offline;
		if(Obj.Mode)
		{
			NewMode = Obj.ModeX;
		}
		if(Obj.ReferenceTimeString)
		{
			ReferenceTimeString = Obj.ReferenceTimeString;
		}
		if(Obj.TargetTimeString)
		{
			TargetTimeString = Obj.TargetTimeString;
		}
		if(Obj.ThreadsActive)
		{
			ThreadsActive = Obj.ThreadsActive;
		}
		if(Obj.GroupsAllActive || Obj.GroupsAllActive == 0 || Obj.GroupsAllActive)
		{
			GroupsAllActive = Obj.GroupsAllActive;
		}
		else
		{
			GroupsAllActive = 1;
		}
		if(Obj.GroupsDisabled)
		{
			GroupsDisabled = Obj.GroupsDisabled;
		}
		if(Obj.nContextSwitchEnabled)
		{
			nContextSwitchEnabled = Obj.nContextSwitchEnabled; 
		}
		else
		{
			nContextSwitchEnabled = 1;
		}
		if(Obj.GroupColors)
		{
			GroupColors = Obj.GroupColors;
		}
		else
		{
			GroupColors = 0;
		}
		if(Obj.nHideHelp)
		{
			nHideHelp = 1;
		}
		if(Obj.ColumnsEnabled)
		{
			ColumnsEnabled = Obj.ColumnsEnabled;
		}
		if(Obj.ToolTipCorner)
		{
			ToolTipCorner = Obj.ToolTipCorner;
		}
		if(Obj.DrawDetailedFlameMode)
		{
			DrawDetailedFlameMode = Obj.DrawDetailedFlameMode;
		}
		if(Obj.DrawDetailedCompareReverse)
		{
			DrawDetailedCompareReverse = Obj.DrawDetailedCompareReverse;
		}
		if(Obj.ThreadOrderNames)
		{
			ThreadOrderNames = Obj.ThreadOrderNames;
		}
		if(Obj.ThreadLogAutoHide)
		{
			ThreadLogAutoHide = Obj.ThreadLogAutoHide;
		}
	}
	SetContextSwitch(nContextSwitchEnabled);
	SetMode(NewMode);
	SetReferenceTime(ReferenceTimeString);
	SetTargetTime(TargetTimeString);
	UpdateGroupColors();
	UpdateThreadLogAutoHide();
}
function WriteCookie()
{
	var C = GetCookie();
	var Obj = new Object();
	Obj.ModeX = Mode;
	Obj.ReferenceTimeString = ReferenceTimeString;
	Obj.TargetTimeString = TargetTimeString;
	Obj.ThreadsActive = ThreadsActive;
	Obj.GroupsDisabled = GroupsDisabled;
	Obj.GroupsAllActive = GroupsAllActive;
	Obj.nContextSwitchEnabled = nContextSwitchEnabled;
	Obj.GroupColors = GroupColors;
	Obj.ColumnsEnabled = ColumnsEnabled;
	Obj.ToolTipCorner = ToolTipCorner;
	Obj.DrawDetailedFlameMode = DrawDetailedFlameMode;
	Obj.DrawDetailedCompareReverse = DrawDetailedCompareReverse;
	Obj.ThreadOrderNames = ThreadOrderNames;
	Obj.ThreadLogAutoHide = ThreadLogAutoHide;
	if(nHideHelp)
	{
		Obj.nHideHelp = 1;
	}
	C.offline = Obj;
	var date = new Date();
	date.setFullYear(2099);
	var cookie = 'fisk=' + JSON.stringify(C) + ';expires=' + date;
	document.cookie = cookie;
	localStorage.setItem("microprofile_fisk", cookie);
}

function WindowRect(x,y,w,h)
{
	var s = {};
	s.x = x;
	s.y = y;
	s.w = w;
	s.h = h;
	return s;
}

function MenuSize(w)
{
	return WindowRect(nWidth / 2 - w / 2, 5, w, nHeight);
}

function TimerMenuSize()
{
	return MenuSize(200 + 5 + FontWidth); //fix menu size
}

function DrawFilterSearch()
{
	FilterSearchSelection = -1;
	FilterSearchPassIndex = -1;
	FilterSearchSelectionMax = 0;
	if(!FilterSearchActive)
		return;
	nHoverToken = -1;

	if(FilterInputSearchLast != FilterInput.value)
	{
		nOffsetFilterSearch = 0;
	}
	FilterInputSearchLast = FilterInput.value;

	var FilterArray = CreateFilter(FilterInput.value);
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = TimerMenuSize();
	var Y = SizeInfo.y;
	var Width = S.TimerNameWidth + S.GroupNameWidth;
	SizeInfo.w = Width;
	var X = SizeInfo.x;

	MoveFilterInputDiv(SizeInfo.x, SizeInfo.y, SizeInfo.w);
	var YStart = Y;
	Y += 35; //todo: measure somehow?
	var MouseX = DetailedViewMouseX;
	var MouseY = DetailedViewMouseY;

	Y -= nOffsetFilterSearch;
	var Count = 0;
	for(var i = 0; i < S.TimerInfo.length; ++i)
	{
		var v = S.TimerInfo[i];
		{
			var Name = v.name;
			var ParentName = S.GroupInfo[v.group].name;
			if(FilterMatch(FilterArray, ParentName + " " + Name))
			{
				if(Y >= YStart)
				{
					Count++;
					var ParentName = S.GroupInfo[v.group].name;
					var ParentColor = 'white';
					var bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
					var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
					var TextY = Y+BoxHeight-FontAscent;
					context.fillStyle = bgcolor;
					context.fillRect(X, Y, Width, BoxHeight);
					context.fillStyle = ParentColor;
					context.fillText(ParentName, X + 2, TextY);
					context.fillStyle = g_Colors[v.cid];
					context.textAlign = 'right';
					context.fillText(Name, X + Width - 2, TextY);
					context.textAlign = 'left';
					if(bMouseIn)
					{
						FilterSearchSelection = i;
					}
				}
				Y += BoxHeight;
				if(Y > nHeight)
					break;
				nColorIndex = 1-nColorIndex;
			}
		}
	}
	FilterSearchSelectionMax = Count;
	SizeInfo.h = Y-SizeInfo.y;
	RequestRedraw();
	return SizeInfo;
}

function CalcAverage()
{
	var Sum = 0;
	var Count = 0;
	for(nLog = 0; nLog < nNumLogs; nLog++)
	{
		StackPos = 0;
		for(var i = 0; i < S.Frames.length; i++)
		{
			var Frame_ = S.Frames[i];			
			var tt = Frame_.tt[nLog];
			var ts = Frame_.ts[nLog];

			var count = tt.length;
			for(var j = 0; j < count; j++)
			{
				var type = tt[j];
				var time = ts[j];
				if(type == 1)
				{
					Stack[StackPos] = time;//store the frame which it comes from
					StackPos++;
				}
				else if(type == 0)
				{
					if(StackPos>0)
					{

						StackPos--;
						var localtime = time - Stack[StackPos];
						Count++;
						Sum += localtime;
					}
				}
			}
		}
	}
	return Sum / Count;

}

function MakeLod(index, MinDelta, TimeArray, TypeArray, IndexArray, LogStart)
{
	if(S.LodData[index])
	{
		console.log("error!!");
	}
	var o = new Object();
	o.MinDelta = MinDelta;
	o.TimeArray = TimeArray;
	o.TypeArray = TypeArray;
	o.IndexArray = IndexArray;
	o.LogStart = LogStart;
	S.LodData[index] = o;
}

function PreprocessBuildSplitArray()
{
	var nNumLogs = S.Frames[0].ts.length;

	ProfileEnter("PreprocessBuildSplitArray");
	var SplitArrays = new Array(nNumLogs);

	for(nLog = 0; nLog < nNumLogs; ++nLog)
	{
		console.log("source log " + nLog + " size " + S.LodData[0].TypeArray[nLog].length);
	}


	for(nLog = 0; nLog < nNumLogs; nLog++)
	{
		var MaxDepth = 1;
		var StackPos = 0;
		var Stack = Array(20);
		var TypeArray = S.LodData[0].TypeArray[nLog];
		var TimeArray = S.LodData[0].TimeArray[nLog];
		var DeltaTimes = new Array(TypeArray.length);

		for(var j = 0; j < TypeArray.length; ++j)
		{
			var type = TypeArray[j];
			var time = TimeArray[j];
			if(type == 1)
			{
				//push
				Stack[StackPos] = time;
				StackPos++;
			}
			else if(type == 0)
			{
				if(StackPos>0)
				{
					StackPos--;
					DeltaTimes[j] = time - Stack[StackPos];
				}
				else
				{
					DeltaTimes[j] = 0;
				}
			}
		}
		DeltaTimes.sort(function(a,b){return b-a;});
		var SplitArray = Array(NumLodSplits);
		var SplitIndex = DeltaTimes.length;

		var j = 0;
		for(j = 0; j < NumLodSplits; ++j)
		{
			SplitIndex = Math.floor(SplitIndex / 2);
			while(SplitIndex > 0 && !DeltaTimes[SplitIndex])
			{
				SplitIndex--;
			}
			if(SplitIndex < SplitMin)
			{
				break;
			}
			//search.. if 0
			var SplitTime = DeltaTimes[SplitIndex];
			if(SplitTime>=0)
			{
				SplitArray[j] = SplitTime;
			}
			else
			{
				SplitArray[j] = SPLIT_LIMIT;
			}
			if(j>0)
			{
				console.assert(SplitArray[j-1] <= SplitArray[j], "must be less");
			}

		}
		for(; j < NumLodSplits; ++j)
		{
			SplitArray[j] = SPLIT_LIMIT;
		}


		SplitArrays[nLog] = SplitArray;
	}
	ProfileLeave();
	return SplitArrays;
}

function PreprocessBuildDurationArray()
{
	var nNumLogs = S.Frames[0].ts.length;
	ProfileEnter("PreprocessBuildDurationArray");
	var DurationArrays = new Array(nNumLogs);
	for(nLog = 0; nLog < nNumLogs; ++nLog)
	{
		var MaxDepth = 1;
		var StackPos = 0;
		var Stack = Array(20);
		var StackIndex = Array(20);
		var TypeArray = S.LodData[0].TypeArray[nLog];
		var TimeArray = S.LodData[0].TimeArray[nLog];
		var DurationArray = Array(S.LodData[0].TypeArray[nLog].length);
		for(var j = 0; j < TypeArray.length; ++j)
		{
			var type = TypeArray[j];
			var time = TimeArray[j];
			if(type == 1)
			{
				//push
				Stack[StackPos] = time;
				StackIndex[StackPos] = j;
				StackPos++;
			}
			else if(type == 0)
			{
				if(StackPos>0)
				{
					StackPos--;
					var Duration = time - Stack[StackPos];
					DurationArray[StackIndex[StackPos]] = Duration;
					DurationArray[j] = Duration;
				}
				else
				{
					DurationArray[j] = 0;
				}
			}
		}
		for(var j = 0; j < StackPos; ++j)
		{
			DurationArray[j] = 0;
		}
		DurationArrays[nLog] = DurationArray;
	}
	ProfileLeave();
	return DurationArrays;

}
function PreprocessLods()
{
	ProfileEnter("PreprocessLods");
	var nNumLogs = S.Frames[0].ts.length;
	var SplitArrays = PreprocessBuildSplitArray();
	var DurationArrays = PreprocessBuildDurationArray();
	var Source = S.LodData[0];
	var SourceLogStart = Source.LogStart;
	var NumFrames = SourceLogStart.length;

	for(var i = 0; i < NumLodSplits-1; ++i)
	{
		var DestLogStart = Array(SourceLogStart.length);
		for(var j = 0; j < DestLogStart.length; ++j)
		{
			DestLogStart[j] = Array(nNumLogs);
		}
		var MinDelta = Array(nNumLogs);
		var TimeArray = Array(nNumLogs);
		var IndexArray = Array(nNumLogs);
		var TypeArray = Array(nNumLogs);



		for(nLog = 0; nLog < nNumLogs; ++nLog)
		{
			var SourceTypeArray = Source.TypeArray[nLog];
			var SourceTimeArray = Source.TimeArray[nLog];
			var SourceIndexArray = Source.IndexArray[nLog];
			var Duration = DurationArrays[nLog];
			console.assert(Duration.length == SourceTypeArray.length, "must be equal!");
			var SplitTime = SplitArrays[nLog][i];

			MinDelta[nLog] = SplitTime;
			if(SplitTime < SPLIT_LIMIT)
			{
				var SourceCount = SourceTypeArray.length;
				var DestTypeArray = Array();
				var DestTimeArray = Array();
				var DestIndexArray = Array();
				var RemapArray = Array(SourceCount);
				var DiscardLast = 0;

				for(var j = 0; j < SourceCount; ++j)
				{
					RemapArray[j] = DestTypeArray.length;
					if(Duration[j] >= SplitTime || (SourceTypeArray[j] == 3 && 0 == DiscardLast))
					{
						DiscardLast = 0;
						DestTypeArray.push(SourceTypeArray[j]);
						DestTimeArray.push(SourceTimeArray[j]);
						DestIndexArray.push(SourceIndexArray[j]);
					}
					else
					{
						DiscardLast = 1;
					}
				}
				TimeArray[nLog] = DestTimeArray;
				IndexArray[nLog] = DestIndexArray;
				TypeArray[nLog] = DestTypeArray;
				for(var j = 0; j < NumFrames; ++j)
				{
					var OldStart = SourceLogStart[j][nLog];
					var NewStart = RemapArray[OldStart];
					var FrameArray = DestLogStart[j];
					FrameArray[nLog] = NewStart;
				}
			}
			else
			{

				for(var j = 0; j < NumFrames; ++j)
				{
					var FrameArray = DestLogStart[j];
	
					FrameArray[nLog] = 0;
				}

			}

		}
		MakeLod(i+1, MinDelta, TimeArray, TypeArray, IndexArray, DestLogStart);
	}
	ProfileLeave();
}
function PreprocessGlobalArray()
{
	ProfileEnter("PreprocessGlobalArray");
	var nNumLogs = S.Frames[0].ts.length;
	var CaptureStart = S.Frames[0].framestart;
	var CaptureEnd = S.Frames[S.Frames.length-1].frameend;
	S.TypeArray = new Array(nNumLogs);
	S.TimeArray = new Array(nNumLogs);
	S.IndexArray = new Array(nNumLogs);
	var StackPos = 0;
	var Stack = Array(20);
	var LogStartArray = new Array(S.Frames.length);
	for(var i = 0; i < S.Frames.length; i++)
	{
		S.Frames[i].LogStart = new Array(nNumLogs);	
		LogStartArray[i] = S.Frames[i].LogStart;

		S.Frames[i].LogEnd = new Array(nNumLogs);
	}
	var MinDelta = Array(nNumLogs);
	for(nLog = 0; nLog < nNumLogs; nLog++)
	{
		MinDelta[nLog] = 0;
		var Discard = 0;
		var TypeArray = new Array();
		var TimeArray = new Array();
		var IndexArray = new Array();
		for(var i = 0; i < S.Frames.length; i++)
		{
			var Frame_ = S.Frames[i];	
			Frame_.LogStart[nLog] = TimeArray.length;
			var CanDiscard = !S.ISGPU[nLog] || Frame_.frameendgpu > 0; //in case of no reference, we cannot discard gpu markers. This happens when there is no gpu/cpu tick reference

			var FrameDiscard = (S.ISGPU[nLog] ? Frame_.frameendgpu : Frame_.frameend) + 33;//if timestamps are more than 33ms after current frame, we assume buffer has wrapped.
			var tt = Frame_.tt[nLog];
			var ts = Frame_.ts[nLog];
			var ti = Frame_.ti[nLog];
			var len = tt.length;
			var DiscardLast = 0;
			for(var xx = 0; xx < len; ++xx)
			{
				var Skip = (tt[i] == 3) ? DiscardLast : (CanDiscard && ts[xx] > FrameDiscard);
				if(Skip)
				{
					Discard++;
					DiscardLast = 1;
				}
				else
				{
					DiscardLast = 0;
					TypeArray.push(tt[xx]);
					TimeArray.push(ts[xx]);
					IndexArray.push(ti[xx]);
				}
			}
			Frame_.LogEnd[nLog] = TimeArray.length;
		}
		S.TypeArray[nLog] = TypeArray;
		S.TimeArray[nLog] = TimeArray;
		S.IndexArray[nLog] = IndexArray;
		if(Discard)
		{
			console.log('discarded ' + Discard + ' markers from ' + S.ThreadNames[nLog]);
		}
	}
	MakeLod(0, MinDelta, S.TimeArray, S.TypeArray, S.IndexArray, LogStartArray);
	ProfileLeave();
}

function InitThreadLogAutoHidden()
{
	let nNumLogs = S.Frames[0].ts.length;
	if(!S.ThreadLogAutoHidden)
	{
		S.ThreadLogAutoHidden = Array();
	}
	while(S.ThreadLogAutoHidden.length < nNumLogs)
	{
		S.ThreadLogAutoHidden.push(0);
	}
	if(!S.ThreadLogTypesUsed)
	{
		S.ThreadLogTypesUsed = Array();
	}
	while(S.ThreadLogTypesUsed.length < nNumLogs)
	{
		S.ThreadLogTypesUsed.push([]);
	}
}

function UpdateThreadLogAutoHide()
{
	let AutoHide = ThreadLogAutoHide;
	let nNumLogs = S.Frames[0].ts.length;
	InitThreadLogAutoHidden();
	for(let nLog = 0; nLog < nNumLogs; ++nLog)
	{
		if(AutoHide)
		{
			let TypesUsed = S.ThreadLogTypesUsed[nLog];
			S.ThreadLogAutoHidden[nLog] = 1;
			for(let i = 0; i < TypesUsed.length; ++i)
			{
				if(TypesUsed[i])
				{
					S.ThreadLogAutoHidden[nLog] = 0;
					break;
				}
			}
		}
		else
		{
			S.ThreadLogAutoHidden[nLog] = 0;
		}
		console.log("AutoHide ", nLog, " ", S.ThreadLogAutoHidden[nLog]);
	}
}

function PreprocessGatherLogTypes()
{
	ProfileEnter("PreprocessGatherLogTypes");
	let TimerCount = S.TimerInfo.length;
	let nNumLogs = S.Frames[0].ts.length;
	InitThreadLogAutoHidden();
	for(let nLog = 0; nLog < nNumLogs; nLog++)
	{
		S.ThreadLogAutoHidden[nLog] = 0;
		let TypesUsed = Array(TimerCount);
		for(let i = 0; i < TypesUsed.length; ++i)
		{
			TypesUsed[i] = 0;
		}
		for(let i = 0; i < S.Frames.length; i++)
		{
			let F = S.Frames[i];
			console.assert(F.tt.length == nNumLogs);
			let Data = F.ti[nLog];
			for(let j in Data)
			{
				let t = Data[j];
				console.assert(t < TimerCount);
				TypesUsed[t] = 1;
			}
		}
		S.ThreadLogTypesUsed[nLog] = TypesUsed;
	}
	ProfileLeave();
}

function PreprocessFindFirstFrames()
{
	ProfileEnter("PreprocesFindFirstFrames");
	//create arrays that show how far back we need to start search in order to get all markers.
	let nNumLogs = S.Frames[0].ts.length;
	for(let i = 0; i < S.Frames.length; i++)
	{
		S.Frames[i].FirstFrameIndex = new Array(nNumLogs);
		for(let j = 0; j < S.Frames[i].FirstFrameIndex.length; ++j)
		{
			S.Frames[i].FirstFrameIndex[j] = 0;
		}
	}

	let StackPos = 0;
	let Stack = Array(20);
	S.MaxStack = Array(nNumLogs);
	
	for(nLog = 0; nLog < nNumLogs; nLog++)
	{
		let MaxStack = 0;
		StackPos = 0;
		for(let i = 0; i < S.Frames.length; i++)
		{
			let Frame_ = S.Frames[i];			
			let tt = Frame_.tt[nLog];
			let count = tt.length;

			let FirstFrame = i;
			if(StackPos>0)
			{
				FirstFrame = Stack[0];
			}
			S.Frames[i].FirstFrameIndex[nLog] = FirstFrame;

			for(let j = 0; j < count; j++)
			{
				let type = tt[j];
				if(type == 1)
				{
					Stack[StackPos] = i;//store the frame which it comes from
					StackPos++;
					if(StackPos > MaxStack)
					{
						MaxStack = StackPos;
					}
				}
				else if(type == 0)
				{
					if(StackPos>0)
					{
						StackPos--;
					}
				}
			}
		}
		S.MaxStack[nLog] = MaxStack;
	}
	S.MaxStack2 = Array(S.MaxStack.length);
	for(let i = 0; i < S.MaxStack2.length; ++i)
		S.MaxStack2[i] = 0;
	S.SecondActive = 0;
	ProfileLeave();
}
function PreprocessMeta()
{
	return;
	MetaLengths = Array(MetaNames.length);
	MetaLengthsAvg = Array(MetaNames.length);
	MetaLengthsMax = Array(MetaNames.length);
	for(var i = 0; i < MetaNames.length; ++i)
	{
		MetaLengths[i] = MetaNames[i].length+1;
		MetaLengthsAvg[i] = MetaNames[i].length+5;
		MetaLengthsMax[i] = MetaNames[i].length+5;
		if(MetaLengths[i]<12)
			MetaLengths[i] = 12;
		if(MetaLengthsAvg[i]<12)
			MetaLengthsAvg[i] = 12;
		if(MetaLengthsMax[i]<12)
			MetaLengthsMax[i] = 12;
	}
	for(var i = 0; i < S.TimerInfo.length; ++i)
	{
		var Timer = S.TimerInfo[i];
		for(var j = 0; j < MetaNames.length; ++j)
		{
			var Len = FormatMeta(Timer.meta[j],0).length + 2;
			var LenAvg = FormatMeta(Timer.meta[j],2).length + 2;
			var LenMax = FormatMeta(Timer.meta[j],0).length + 2;
			if(Len > MetaLengths[j])
			{
				MetaLengths[j] = Len;
			}
			if(LenAvg > MetaLengthsAvg[j])
			{
				MetaLengthsAvg[j] = LenAvg;
			}
			if(LenMax > MetaLengthsMax[j])
			{
				MetaLengthsMax[j] = LenMax;
			}
		}
	}
}

function StringHash(s) //note: matching code in microprofile.cpp: uint32_t MicroProfileStringHash(const char* pString)
{ 
	let h = 0xfeedba3e;
	for(let i = 0; i < s.length; ++i)
	{
		h = s.charCodeAt(i) + (h << 5) - h;
		h = h & h;
	}
	return Math.abs(h);
}

function StringColorIndex(Name)
{
	let h = StringHash(Name);
	let cidx = Math.floor(360*(h  / (1<<32-1)) );
	return cidx;
}

function ColorFromString(Name, S, L)
{
	let H = StringColorIndex(Name);
	return "hsl(" + H + "," + S + "%, " + L+ "%)";
}
function LerpColor(v)
{
	let R_0 = 0;
	let G_0 = 1;
	let B_0 = 0;

	let R_1 = 1;
	let G_1 = 0.5;
	let B_1 = 0;

	let R_2 = 1;
	let G_2 = 0;
	let B_3 = 0;
	let R;
	let G;
	if(v < 0.5)
	{
		v *= 2;
		let v0 = (1-v);
		R = R_0 * v0 + R_1 * v;
		G = G_0 * v0 + G_1 * v;

	}
	else
	{
		v = (v-0.5) * 2;
		let v0 = (1-v);
		R = R_1 * v0 + R_2 * v;
		G = G_1 * v0 + G_2 * v;
	}
	R *= 255;
	G *= 255;
	return "rgb(" + R.toFixed(0) + "," + G.toFixed(0) + ",0)";

}

function PreprocessThreadColors(S)
{
	S.ThreadColors = Array(S.ThreadNames.length);
	for(let i = 0; i < S.ThreadNames.length; ++i)
	{
		var cidx = StringColorIndex(S.ThreadNames[i]);
		var color = "hsl(" + cidx + ",50%, 60%)";
		var coloroff = "hsl(" + cidx + ",85%, 50%)";
		var colordark = "hsl(" + cidx + ",80%, 30%)";
		var colordark_cid = GetColorIndex(colordark);
		S.ThreadColors[i] = {"color":color, "colordark":colordark, "cidx":cidx, "gradient":null,"gradientoff":null, "coloroff":coloroff, "colordark_cid":colordark_cid};
	}
}

function PreprocessWidths()
{
	let context = CanvasHistory.getContext('2d');
	context.font = Font;
	S.TimerNameWidth = 0;
	S.GroupNameWidth = 0;

	for(let i in S.TimerInfo)
	{
		let str = S.GroupInfo[S.TimerInfo[i].group].name + "XXX" + S.TimerInfo[i].name;		
		let width = context.measureText(str).width;
		let widthname = context.measureText(S.TimerInfo[i].name).width;
		S.TimerInfo[i].wtotal = width;
		S.TimerInfo[i].w = widthname;
		S.TimerNameWidth = Math.max(S.TimerNameWidth, widthname);
	}
	for(let i in S.GroupInfo)
	{
		let widthname = context.measureText(S.TimerInfo[i].name).width;
		S.GroupNameWidth = Math.max(S.GroupNameWidth, widthname);
	}
	for(let i in S.ThreadNames)
	{
		let w = context.measureText(S.ThreadNames[i]).width;
		S.ThreadNameWidth = Math.max(S.ThreadNameWidth, w);
	}
	for(let i in S.CategoryInfo)
	{
		let w = context.measureText(S.CategoryInfo[i]).width;
		S.ThreadCategoryWidth = Math.max(S.ThreadCategoryWidth, w);
	}
}

function PreprocessTimeline()
{
	Timeline.Times = S.TimelineArray;
	Timeline.Ids = S.TimelineIdArray;
	Timeline.Colors = new Array(S.TimelineArray.length);
	Timeline.Names = S.TimelineNames;
	Timeline.Ends = new Array(S.TimelineArray.length);
	Timeline.Pairs = new Array(S.TimelineArray.length);
	Timeline.Tracks = new Array();
	Timeline.Positions = new Array();
	for(var i = 0; i < Timeline.Times.length; ++i)
	{
		Timeline.Positions[i] = -1;
		Timeline.Ends[i] = -1;
		Timeline.Pairs[i] = -1;
		var Color = S.TimelineColorArray[i]
		if(!Color || Color == '')
		{
			Color = '#777777'
		}
		Timeline.Colors[i] = GetColorIndex(Color);
		if(null == Timeline.Colors[i])
			debugger;
		Timeline.Colors[i] = GetColorIndex(Color);
	}


	var LinearizeTrack = function(Track)
	{
		var b = new Array();
		var e = new Array();
		// var n = new Array();
		var k = new Array();
		var h = Track.h;
		while(h != -1)
		{
			b.push(Track.b[h]);
			e.push(Track.e[h]);
			k.push(Track.k[h]);

			h = Track.n[h];
		}
	}

	var CreateTrack = function()
	{
		var Track = {};
		Track.b = new Array();
		Track.e = new Array();
		Track.n = new Array();
		Track.k = new Array();
		Track.h = -1;
		return Track;
	}


	var TryAddToTrack = function(Track, b, e, k)
	{
		var Invalid = 0;
		var prev = -1;
		var next = Track.h;
		while(next != -1)
		{
			var B = Track.b[next];
			var E = Track.e[next];
			if(Math.max(B, b) < Math.min(E, e))
			{
				Invalid = 1;
				break;
			}
			if(E < b)
			{
				//should be after next, continue
			}else if(e < B)
			{
				//insert at this spot
				break;
			}
			prev = next;
			next = Track.n[next];
		}
		if(!Invalid)
		{
			var idx = Track.b.length;
			Track.b.push(b);
			Track.e.push(e);
			Track.k.push(k);
			Track.n.push(next);
			if(prev == -1)
			{
				Track.h = idx;
			}
			else
			{
				var h = Track.n[prev];
				if(h != next)
				{
					debugger;
				}
				Track.n[prev] = idx;
			}
			return true;
		}
		else
		{
			return false;
		}
	};


	var AddToTrack = function(b, e, k)
	{
		var Tracks = Timeline.Tracks;

		for(var i = 0; i < Tracks.length; ++i)
		{
			var Track = Tracks[i];
			if(TryAddToTrack(Track, b, e, k))
			{
				return i;
			}
		}
		var NewTrack = CreateTrack();
		var i = Timeline.Tracks.length;
		Timeline.Tracks.push(NewTrack);
		if(!TryAddToTrack(NewTrack, b, e, k))
		{
			debugger;
		}
		return i;

	};





	var Matching = {};
	var Pairs = Timeline.Pairs;
	var Ids = Timeline.Ids;
	var Ends = Timeline.Ends;
	var Times = Timeline.Times;
	if(Ids)
	{
		for(var i = 0; i < Ids.length; ++i)
		{
			Pairs[i] = -1;
			var ID = Ids[i];
			var match = Matching[ID];
			if(match)
			{
				if(match > Ids.length || match < 0)
					debugger;
				Pairs[match] = i;
				Pairs[i] = match;
				if(match > i)
					debugger;
				Ends[match] = Times[i];
				var Track = AddToTrack(Times[match], Ends[match], match);
				Timeline.Positions[match] = Track;
			}
			else
			{
				Matching[ID] = i;
			}
		}
	}
}

function Preprocess_S()
{
	console.log('preprocessing\n');	
	S.LodData = new Array();
	S.CSwitchCache = {};
	S.CSwitchOnlyThreads = [];
	S.GroupNameWidth = 200;
	S.TimerNameWidth = 200;
	S.ThreadNameWidth = 150;
	S.ThreadCategoryWidth = 150;

	PreprocessCalculateAllTimers();
	PreprocessFindFirstFrames();
	PreprocessGatherLogTypes();

	PreprocessGlobalArray();
	PreprocessLods();
	PreprocessMeta();
	PreprocessThreadColors(S);

	PreprocessContextSwitchCache();
	PreprocessWidths();

	UpdateThreadLogAutoHide();

	console.log('preprocessing done\n');
	console.log("SS " + S.LodData.length);
}
function Preprocess()
{
	var ProfileModeOld = ProfileMode;
	ProfileMode = 1;
	ProfileModeClear();
	ProfileEnter("Preprocess");
	Preprocess_S();
	UpdateReferenceTime();

	ProfileLeave();
	ProfileModeDump();
	ProfileMode = ProfileModeOld;
	PreprocessTimeline();
	Initialized = 1;
}

function ToggleColumn(idx, isMeta)
{
	ColumnsEnabled[idx] = !ColumnsEnabled[idx];
	UpdateColumnsMenu();
	WriteCookie();
	Invalidate = 0;
}
function UpdateColumnsMenu()
{
	var ulColumnMenu = document.getElementById('ColumnsSubMenu');
	var Lis = ulColumnMenu.getElementsByTagName('li');
	for(var i = 0; i < Lis.length; ++i)
	{
		if(ColumnsEnabled[i])
		{
			Lis[i].style['text-decoration'] = 'underline';
		}
		else
		{
			Lis[i].style['text-decoration'] = 'none';
		}
	}
}

function ResetColumnWidth()
{
	if(ColumnsWidth)
	{
		for(var i = 0; i < ColumnsWidth.length; ++i)
		{
			ColumnsWidth[i] = 0;
		}
	}
}
var Comp0 = new Date();
var Comp1 = new Date();
var Comp2 = new Date();
var Comp3 = new Date();

function SetGlobal(Name, Value)
{
	this[Name] = Value;
}


//compare todo:
//	**Match threads
//	**match tokens.
//	**Calculate shared screen positions.
//	**Invert second pass
//	**fix hovertoken
//	**fix range display
// 	todo: isgpu
//  todo: group numtimers.
//	implement timer view

function CompareFixup(S0, S1) // S0 is the reference
{
	var GroupRemap = Array(S1.GroupInfo.length);
	var ThreadRemap = Array(S1.ThreadNames.length);
	var TimerRemap = Array(S1.TimerInfo.length);
	GroupRemapReverse = Array(S0.GroupInfo.length + S1.GroupInfo.length);
	TimerRemapReverse = Array(S0.TimerInfo.length + S1.TimerInfo.length);
	ThreadRemapReverse = Array(S0.ThreadNames.length + S1.ThreadNames.length);

	for(var i = 0; i < GroupRemapReverse.length; ++i)
		GroupRemapReverse[i] = -1;
	for(var i = 0; i < TimerRemapReverse.length; ++i)
		TimerRemapReverse[i] = -1;
	for(var i = 0; i < ThreadRemapReverse.length; ++i)
		ThreadRemapReverse[i] = -1;



	// var dump = function(SS, str)
	// {
	// 	console.log(str);
	// 	for(var i = 0; i < SS.GroupInfo.length; ++i)
	// 	{
	// 		console.log("G" + i + ":" + SS.GroupInfo[i].name);
	// 	}
	// 	for(var i = 0; i < SS.TimerInfo.length; ++i)
	// 	{
	// 		console.log("T" + i + ":" + SS.TimerInfo[i].name);
	// 	}
	
	// };
	// dump(S0, "S0be4");
	// dump(S1, "S1be4");

	for(var i = 0; i < S1.GroupInfo.length; ++i)
	{
		var n = S1.GroupInfo[i].name;
		var idx = S0.GroupInfo.findIndex(function(G){ return G.name == n; });
		if(idx != -1)
		{
			GroupRemap[i] = idx;
		}
		else
		{
			GroupRemap[i] = S0.GroupInfo.length;
			S0.GroupInfo.push(S1.GroupInfo[i]);
		}
		GroupRemapReverse[GroupRemap[i]] = i;
	}
	// for(var i = 0; i < S0.TimerInfo.length; ++i)
	// {
	// 	var n = S0.TimerInfo[i].name;	
	// 	console.log('OG ', n, ' idx ', i);
	// }

	for(var i = 0; i < S1.TimerInfo.length; ++i)
	{
		var TI = S1.TimerInfo[i];
		var n = TI.name;
		var g = GroupRemap[TI.group];
		var idx = S0.TimerInfo.findIndex(function(T){ return T.name == n && T.group == g; });
		TI.group = GroupRemap[TI.group];
		if(idx != -1)
		{
			TimerRemap[i] = idx;
		}
		else
		{
			console.log('added timer ' + TI.name + ' idx ' + S0.TimerInfo.length);
			TimerRemap[i] = S0.TimerInfo.length;
			TI.id = S0.TimerInfo.length;
			S0.TimerInfo.push(TI);
		}
		TimerRemapReverse[TimerRemap[i]] = i;
	}

	var NumThreads = S0.ThreadNames.length;


	for(var i = 0; i < S1.ThreadNames.length; ++i)
	{
		var n = S1.ThreadNames[i];
		var idx = S0.ThreadNames.indexOf(n);
		if(idx != -1)
		{
			ThreadRemap[i] = idx;
		}
		else
		{
			ThreadRemap[i] = S0.ThreadNames.length;
			S0.ThreadNames.push(n);
			NumThreads++;
		}
		ThreadRemapReverse[ThreadRemap[i]] = i;
	}


	PreprocessThreadColors(S0);
	PreprocessThreadColors(S1);

	var Fix = function(a, r)
	{
		for(var i in a)
		{
			a[i] = r[a[i]];
		}
	};
	var FixArray = function(a, s)
	{
		if(!s)
			return;
		for(var i = 0; i < a.length; ++i)
		{
			if(!a[i])
				a[i] = [];
		}
		while(a.length != s)
			a.push([]);
	};

	for(var i in S0.Frames)
	{
		var F = S0.Frames[i];
		FixArray(F.ti, NumThreads);
		FixArray(F.ts, NumThreads);
		FixArray(F.tt, NumThreads);
	}

	for(var i in S1.Frames)
	{
		var F = S1.Frames[i];
		var ti = F.ti;
		var ts = F.ts;
		var tt = F.tt;

		F.ti = new Array(NumThreads);
		F.ts = new Array(NumThreads);
		F.tt = new Array(NumThreads);
		
		for(var j in ti)
		{

			Fix(ti[j], TimerRemap);
			F.ti[ThreadRemap[j]] = ti[j];
		}
		FixArray(F.ti, NumThreads);
		for(var i = 0; i < NumThreads; ++i)
		{
			if(!F.ti[i])
				debugger;
		}
		for(var j in ts)
		{
			F.ts[ThreadRemap[j]] = ts[j];
		}
		FixArray(F.ts, NumThreads);
		for(var i = 0; i < NumThreads; ++i)
		{
			if(!F.ts[i])
				debugger;
		}
		for(var j in tt)
		{
			F.tt[ThreadRemap[j]] = tt[j];
		}
		FixArray(F.tt, NumThreads);

		for(var i = 0; i < NumThreads; ++i)
		{
			if(!F.tt[i])
				debugger;
		}
		//assert length is the same
		if(F.tt.length != S0.Frames[0].tt.length)
		{
			debugger;
		}
		if(F.ti.length != S0.Frames[0].ti.length)
		{
			debugger;
		}
		if(F.ts.length != S0.Frames[0].ts.length)
		{
			debugger;
		}
		// console.log('log ' + i +  ':', F.ts.length, ':', F.ti.length, ':', F.tt.length);
	}

	var NewTimerInfo = Array(S0.TimerInfo.length);
	for(var i = 0; i < S1.TimerInfo.length; ++i)
	{
		let xx = TimerRemap[i];
		NewTimerInfo[xx] = S1.TimerInfo[i];
		NewTimerInfo[xx].id = xx;
		NewTimerInfo[xx].group = S0.TimerInfo[xx].group;
		
		if(NewTimerInfo[xx].id != S0.TimerInfo[xx].id)
		{
			debugger;
		}
		if(NewTimerInfo[xx].group != S0.TimerInfo[xx].group)
		{
			debugger;
		}

	}
	for(var i = 0; i < NewTimerInfo.length; ++i)
	{
		if(!NewTimerInfo[i])
		{
			NewTimerInfo[i] = CloneTimer(S0.TimerInfo[i]);
			NewTimerInfo[i].id = i;
			if(NewTimerInfo[i].id != S0.TimerInfo[i].id)
			{
				debugger;
			}
			if(NewTimerInfo[i].group != S0.TimerInfo[i].group)
			{
				debugger;
			}
	
		}
	}
	S1.TimerInfo = NewTimerInfo;
	var NewGroupInfo = Array(S0.GroupInfo.length);

	for(let i = 0; i < S1.GroupInfo.length; ++i)
	{
		let xx = GroupRemap[i];
		NewGroupInfo[xx] = S1.GroupInfo[i];
		NewGroupInfo[xx].id = xx;
	}
	for(let i = 0; i < NewGroupInfo.length; ++i)
	{
		if(!NewGroupInfo[i])
		{
			NewGroupInfo[i] = CloneGroup(S0.GroupInfo[i]);
			NewGroupInfo[i].id = i;
		}
	}
	S1.GroupInfo = NewGroupInfo;
	for(let i = 0; i < S1.GroupInfo.length; ++i)
	{
		if(S1.GroupInfo[i].id != i)
			debugger;
		if(S0.GroupInfo[i].id != i)
			debugger;
	}

	//debugging:
	if(S0.TimerInfo.length != S1.TimerInfo.length)
		debugger;
	for(let i = 0; i < S0.TimerInfo.length; ++i)
	{
		let t0 = S0.TimerInfo[i];
		let t1 = S1.TimerInfo[i];
		if(t0.id != t1.id)
			debugger;
		if(t0.name != t1.name)
			debugger;
		if(t0.group != t1.group)
			debugger;
	}

	if(S0.GroupInfo.length != S1.GroupInfo.length)
		debugger;
	for(let i = 0; i < S0.GroupInfo.length; ++i)
	{
		let t0 = S0.GroupInfo[i];
		let t1 = S1.GroupInfo[i];
		if(t0.id != t1.id)
			debugger;
		if(t0.name != t1.name)
			debugger;
	}




}
function ReadHtmlFile(e)
{
	var File = e.target.files[0];
	if (!File)
	{
		return;
	}
	var Reader = new FileReader();
	Reader.onload = function(e) {
		if(!ParseCompareData(e.target.result))
		{
			ShowFlashMessage("Failed to parse file", 50);
		}
	};
	Reader.onprogress = function(e)
	{
		var m = e.loaded + ":" + e.total + " :: " + e.lengthComputable;
		console.log(m);
	};
	C0 = new Date();
	Reader.readAsText(File);
}

function ParseCompareData(Data)
{
	S.AA = "Original";
	Comp1 = new Date();
	
	var idxbegin = Data.indexOf("//EBEGIN");
	if(idxbegin == -1)
		return false;
	idxbegin += "//EBEGIN".length + 1;
	var idxend = Data.indexOf("//EEND", idxbegin);
	if(idxend == -1)
		return false;

	Comp2 = new Date();
	var SOriginal = S;
	S = {};

	var idx = idxbegin;
	while(idx < idxend)
	{
		var idx0 = Data.indexOf('\n', idx);
		var s = Data.substring(idx, idx0);
		// console.log(s);
		eval(s);
		idx = idx0+1;
	}

	S.AA = "Parsed";
	var SParsed = S;   //note: the calls to eval, will make a new "S" in local scope.
	SetGlobal("S", S); //Set global S to point to new object, while its preprocessed
	Comp3 = new Date();


	CompareFixup(SOriginal, SParsed);
	Preprocess_S();

	var MaxStack2 = SParsed.MaxStack;

	var Comp4 = new Date();
	SetGlobal("S2", SParsed);
	SetGlobal("S", SOriginal);

	Preprocess_S(); //since indices are rebased, a repreprocessing is needed.

	SOriginal.MaxStack2 = SParsed.MaxStack;
	SParsed.MaxStack2 = SOriginal.MaxStack;
	SOriginal.SecondActive = 1;


	ThreadOrderSort();

	console.log('file loaded ' + idxbegin + ' ' + idxend );
	console.log('time ' + (Comp1 - Comp0));
	console.log('time ' + (Comp2 - Comp1));
	console.log('time ' + (Comp3 - Comp2).toFixed(2));
	console.log('time ' + (Comp4 - Comp3).toFixed(2));
	document.getElementById('file-input').removeEventListener('change', ReadHtmlFile, false);
	let fname = document.getElementById('file-input').value;
	let index = 1+Math.max(fname.lastIndexOf('/'), fname.lastIndexOf('\\'));
	if(index < fname.length)
		fname = fname.substring(index);
	let CompStr = 'Compare [' + fname + ']';
	RequestRedraw();
	return true;
}

function ComparePrompt()
{
	document.getElementById('file-input').addEventListener('change', ReadHtmlFile, false);
	document.getElementById('file-input').click();
}


var mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel" //FF doesn't recognize mousewheel as of FF3.x

CanvasDetailedView.addEventListener('mousemove', MouseMove, false);
CanvasDetailedView.addEventListener('mousedown', function(evt) { MouseButton(true, evt); });
CanvasDetailedView.addEventListener('mouseup', function(evt) { MouseButton(false, evt); } );
CanvasDetailedView.addEventListener('mouseout', MouseOut);
CanvasDetailedView.addEventListener("contextmenu", function (e) { e.preventDefault(); }, false);
CanvasDetailedView.addEventListener(mousewheelevt, MouseWheel, false);
CanvasHistory.addEventListener('mousemove', MouseMove);
CanvasHistory.addEventListener('mousedown', function(evt) { MouseButton(true, evt); });
CanvasHistory.addEventListener('mouseup', function(evt) { MouseButton(false, evt); } );
CanvasHistory.addEventListener('mouseout', MouseOut);
CanvasHistory.addEventListener("contextmenu", function (e) { e.preventDefault(); }, false);
CanvasHistory.addEventListener(mousewheelevt, MouseWheel, false);
CanvasMenu.addEventListener('mousemove', MouseMove);
CanvasMenu.addEventListener('mousedown', function(evt) { MouseButton(true, evt); });
CanvasMenu.addEventListener('mouseup', function(evt) { MouseButton(false, evt); } );
CanvasMenu.addEventListener('mouseout', MouseOut);
CanvasMenu.addEventListener("contextmenu", function (e) { e.preventDefault(); }, false);
CanvasMenu.addEventListener(mousewheelevt, MouseWheel, false);
FilterInputTimer.addEventListener('keyup', FilterKeyUp);
FilterInputGroup.addEventListener('keyup', FilterKeyUp);
window.addEventListener('keydown', KeyDown);
window.addEventListener('keyup', KeyUp);
window.addEventListener('resize', ResizeCanvas, false);

InitGroups();
ReadCookie();
MeasureFont();
ThreadOrderSort();
InitOrderArrays();
InitMenu();
InitFrameInfo();
ResizeCanvas();
Preprocess();
OnPageReady();
Draw(1);


</script>
</body>
</html>      